Skip to content

Commit e2462fa

Browse files
mvicknrclaude
andauthored
chore: Set up go agent to instrument go app (#24)
* chore: Initial test Co-Authored-By: Claude <noreply@anthropic.com> * chore: Add log forwarding * chore: Handle logging a different way * fix: Fixing broken test * chore: Using staging host * chore: Cleanup * chore: Cleanup --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4b02087 commit e2462fa

File tree

7 files changed

+193
-19
lines changed

7 files changed

+193
-19
lines changed

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ inputs:
2323
description: 'Enable Go build cache'
2424
required: false
2525
default: 'true'
26+
apm-control-nr-license-key:
27+
description: 'New Relic APM license key for instrumentation of this action (optional, pass from secrets)'
28+
required: false
29+
default: ''
2630
runs:
2731
using: 'composite'
2832
steps:
@@ -157,6 +161,7 @@ runs:
157161
INPUT_AGENT_TYPE: ${{ inputs.agent-type }}
158162
INPUT_VERSION: ${{ inputs.version }}
159163
NEWRELIC_TOKEN: ${{ steps.newrelic-auth.outputs.token }}
164+
APM_CONTROL_NR_LICENSE_KEY: ${{ inputs.apm-control-nr-license-key }}
160165
run: |
161166
set -e
162167
cd ${{ github.action_path }}

cmd/agent-metadata-action/main.go

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"time"
910

1011
"agent-metadata-action/internal/client"
1112
"agent-metadata-action/internal/config"
1213
"agent-metadata-action/internal/loader"
14+
"agent-metadata-action/internal/logging"
1315
"agent-metadata-action/internal/models"
16+
17+
"github.com/newrelic/go-agent/v3/newrelic"
1418
)
1519

1620
// metadataClient interface for testing
@@ -24,22 +28,89 @@ var createMetadataClientFunc = func(baseURL, token string) metadataClient {
2428
return client.NewInstrumentationClient(baseURL, token)
2529
}
2630

31+
// initNewRelic initializes the New Relic application
32+
// Returns nil if APM_CONTROL_NR_LICENSE_KEY is not set (silent no-op mode)
33+
func initNewRelic() *newrelic.Application {
34+
licenseKey := config.GetNRAgentLicenseKey()
35+
if licenseKey == "" {
36+
_, _ = fmt.Fprintln(os.Stderr, "::warn::Failed to init New Relic - missing license key")
37+
return nil
38+
}
39+
40+
// Hardcode staging environment
41+
err := config.SetNRAgentHost()
42+
if err != nil {
43+
_, _ = fmt.Fprintf(os.Stderr, "::warn::Failed to init New Relic, missing host: %v\n", err)
44+
return nil
45+
}
46+
fmt.Println("::notice::Using New Relic staging environment")
47+
48+
app, err := newrelic.NewApplication(
49+
newrelic.ConfigAppName("agent-metadata-action"),
50+
newrelic.ConfigLicense(licenseKey),
51+
newrelic.ConfigDebugLogger(os.Stdout),
52+
newrelic.ConfigDistributedTracerEnabled(true),
53+
newrelic.ConfigAppLogForwardingEnabled(true),
54+
newrelic.ConfigFromEnvironment(), // This reads NEW_RELIC_HOST
55+
newrelic.ConfigLabels(map[string]string{
56+
"team": "APM Control Team",
57+
}),
58+
)
59+
60+
if err != nil {
61+
_, _ = fmt.Fprintf(os.Stderr, "::warn::Failed to init New Relic: %v\n", err)
62+
return nil
63+
}
64+
65+
fmt.Println("::notice::New Relic APM enabled - waiting for connection...")
66+
67+
// Wait for the app to connect (max 10 seconds)
68+
if err := app.WaitForConnection(10 * time.Second); err != nil {
69+
fmt.Printf("::warn::New Relic connection timeout: %v - will try to send data anyway\n", err)
70+
} else {
71+
fmt.Println("::notice::New Relic connected successfully")
72+
}
73+
74+
return app
75+
}
76+
2777
func main() {
28-
if err := run(); err != nil {
78+
nrApp := initNewRelic()
79+
80+
if err := run(nrApp); err != nil {
2981
_, _ = fmt.Fprintf(os.Stderr, "::error::%v\n", err)
3082
os.Exit(1)
3183
}
84+
85+
if nrApp != nil {
86+
fmt.Println("::notice::Shutting down New Relic - waiting up to 15 seconds to send data...")
87+
nrApp.Shutdown(15 * time.Second)
88+
fmt.Println("::notice::New Relic shutdown complete")
89+
}
3290
}
3391

34-
func run() error {
92+
func run(nrApp *newrelic.Application) error {
93+
// Create context
94+
ctx := context.Background()
95+
96+
// Start transaction if New Relic is enabled
97+
if nrApp != nil {
98+
txn := nrApp.StartTransaction("agent-metadata-action")
99+
defer txn.End()
100+
101+
// Add transaction to context for logging
102+
ctx = newrelic.NewContext(ctx, txn)
103+
logging.Debug(ctx, "New Relic transaction started")
104+
defer logging.Debug(ctx, "New Relic transaction ended")
105+
}
106+
35107
// Validate required environment and setup
36-
workspace, token, err := validateEnvironment()
108+
workspace, token, err := validateEnvironment(ctx)
37109
if err != nil {
38110
return err
39111
}
40112

41113
// Create metadataClient
42-
ctx := context.Background()
43114
metadataClient := createMetadataClientFunc(config.GetMetadataURL(), token)
44115

45116
// Determine which flow to execute
@@ -54,7 +125,7 @@ func run() error {
54125
}
55126

56127
// validateEnvironment checks required environment variables and workspace
57-
func validateEnvironment() (workspace string, token string, err error) {
128+
func validateEnvironment(ctx context.Context) (workspace string, token string, err error) {
58129
workspace = config.GetWorkspace()
59130
if workspace == "" {
60131
return "", "", fmt.Errorf("GITHUB_WORKSPACE is required but not set")
@@ -69,13 +140,13 @@ func validateEnvironment() (workspace string, token string, err error) {
69140
return "", "", fmt.Errorf("NEWRELIC_TOKEN is required but not set")
70141
}
71142

72-
fmt.Println("::notice::Environment validated successfully")
143+
logging.Notice(ctx, "Environment validated successfully")
73144
return workspace, token, nil
74145
}
75146

76147
// runAgentFlow handles the agent repository workflow
77148
func runAgentFlow(ctx context.Context, client metadataClient, workspace, agentType, agentVersion string) error {
78-
fmt.Println("::debug::Running agent repository flow")
149+
logging.Debug(ctx, "Running agent repository flow")
79150

80151
// Check for .fleetControl directory
81152
fleetControlPath := filepath.Join(workspace, config.GetRootFolderForAgentRepo())
@@ -88,15 +159,15 @@ func runAgentFlow(ctx context.Context, client metadataClient, workspace, agentTy
88159
if err != nil {
89160
return fmt.Errorf("failed to read configuration definitions: %w", err)
90161
}
91-
fmt.Printf("::notice::Loaded %d configuration definitions\n", len(configs))
162+
logging.Noticef(ctx, "Loaded %d configuration definitions", len(configs))
92163

93164
// Load agent control definitions (optional)
94165
agentControl, err := loader.ReadAgentControlDefinitions(workspace)
95166
if err != nil {
96-
fmt.Printf("::warn::Unable to load agent control definitions: %v - continuing without them\n", err)
167+
logging.Warnf(ctx, "Unable to load agent control definitions: %v - continuing without them", err)
97168
agentControl = nil
98169
} else {
99-
fmt.Printf("::notice::Loaded %d agent control definitions\n", len(agentControl))
170+
logging.Noticef(ctx, "Loaded %d agent control definitions", len(agentControl))
100171
}
101172

102173
// Build metadata
@@ -113,13 +184,13 @@ func runAgentFlow(ctx context.Context, client metadataClient, workspace, agentTy
113184
return fmt.Errorf("failed to send metadata for %s: %w", agentType, err)
114185
}
115186

116-
fmt.Printf("::notice::Successfully sent metadata for %s version %s\n", agentType, agentVersion)
187+
logging.Noticef(ctx, "Successfully sent metadata for %s version %s", agentType, agentVersion)
117188
return nil
118189
}
119190

120191
// runDocsFlow handles the documentation repository workflow
121192
func runDocsFlow(ctx context.Context, client metadataClient) error {
122-
fmt.Println("::debug::Running documentation flow")
193+
logging.Debug(ctx, "Running documentation flow")
123194

124195
// Load metadata from changed MDX files
125196
metadataList, err := loader.LoadMetadataForDocs()
@@ -128,23 +199,23 @@ func runDocsFlow(ctx context.Context, client metadataClient) error {
128199
}
129200

130201
if len(metadataList) == 0 {
131-
fmt.Println("::notice::No metadata changes detected")
202+
logging.Notice(ctx, "No metadata changes detected")
132203
return nil
133204
}
134205

135-
fmt.Printf("::notice::Processing %d metadata entries\n", len(metadataList))
206+
logging.Noticef(ctx, "Processing %d metadata entries", len(metadataList))
136207

137208
// Send each metadata entry separately
138209
successCount := 0
139210
for _, entry := range metadataList {
140211
if err := sendDocsMetadata(ctx, client, entry); err != nil {
141-
fmt.Printf("::warn::Failed to send metadata for %s: %v\n", entry.AgentType, err)
212+
logging.Warnf(ctx, "Failed to send metadata for %s: %v", entry.AgentType, err)
142213
continue
143214
}
144215
successCount++
145216
}
146217

147-
fmt.Printf("::notice::Successfully sent %d of %d metadata entries\n", successCount, len(metadataList))
218+
logging.Noticef(ctx, "Successfully sent %d of %d metadata entries", successCount, len(metadataList))
148219
return nil
149220
}
150221

@@ -162,7 +233,7 @@ func sendDocsMetadata(ctx context.Context, client metadataClient, entry loader.M
162233
return err
163234
}
164235

165-
fmt.Printf("::notice::Sent metadata for %s version %s\n", entry.AgentType, version)
236+
logging.Noticef(ctx, "Sent metadata for %s version %s", entry.AgentType, version)
166237
return nil
167238
}
168239

cmd/agent-metadata-action/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func TestRun_InvalidEnvironment(t *testing.T) {
168168
t.Setenv("NEWRELIC_TOKEN", "mock-token-for-testing")
169169

170170
// Method under test
171-
err := run()
171+
err := run(nil)
172172

173173
assert.Error(t, err)
174174
assert.Contains(t, err.Error(), "workspace directory does not exist")
@@ -230,7 +230,7 @@ func TestValidateEnvironment(t *testing.T) {
230230
t.Setenv("NEWRELIC_TOKEN", tt.token)
231231

232232
// Method under test
233-
gotWorkspace, gotToken, err := validateEnvironment()
233+
gotWorkspace, gotToken, err := validateEnvironment(context.Background())
234234

235235
if tt.wantErr {
236236
assert.Error(t, err)

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,12 @@ require (
99

1010
require (
1111
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/newrelic/go-agent/v3 v3.42.0 // indirect
1213
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
golang.org/x/net v0.25.0 // indirect
15+
golang.org/x/sys v0.20.0 // indirect
16+
golang.org/x/text v0.15.0 // indirect
17+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
18+
google.golang.org/grpc v1.65.0 // indirect
19+
google.golang.org/protobuf v1.34.2 // indirect
1320
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/newrelic/go-agent/v3 v3.42.0 h1:aA2Ea1RT5eD59LtOS1KGFXSmaDs6kM3Jeqo7PpuQoFQ=
4+
github.com/newrelic/go-agent/v3 v3.42.0/go.mod h1:sCgxDCVydoKD/C4S8BFxDtmFHvdWHtaIz/a3kiyNB/k=
35
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
68
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
9+
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
10+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
11+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
12+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13+
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
14+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
15+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
16+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
17+
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
18+
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
19+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
20+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
721
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
822
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
923
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/config/env.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ func GetEventPath() string {
2929
return os.Getenv("GITHUB_EVENT_PATH")
3030
}
3131

32+
// GetToken loads the newrelic token from the environment variables
3233
func GetToken() string {
3334
return os.Getenv("NEWRELIC_TOKEN")
3435
}
36+
37+
// GetNRAgentLicenseKey gets the license key to use the go agent and monitor this app
38+
func GetNRAgentLicenseKey() string {
39+
return os.Getenv("APM_CONTROL_NR_LICENSE_KEY")
40+
}
41+
42+
// SetNRAgentHost sets the host to use for the go agent that will be used to monitor this app
43+
func SetNRAgentHost() error {
44+
err := os.Setenv("NEW_RELIC_HOST", "staging-collector.newrelic.com")
45+
if err != nil {
46+
return err
47+
}
48+
return nil
49+
}

internal/logging/logging.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/newrelic/go-agent/v3/newrelic"
8+
)
9+
10+
// Log logs to both console (GitHub Actions format) and New Relic
11+
// Extracts the New Relic transaction from context if available
12+
func Log(ctx context.Context, level, message string) {
13+
// Always log to console for GitHub Actions
14+
fmt.Printf("::%s::%s\n", level, message)
15+
16+
// Also send to New Relic if transaction exists in context
17+
if txn := newrelic.FromContext(ctx); txn != nil {
18+
txn.RecordLog(newrelic.LogData{
19+
Message: message,
20+
Severity: level,
21+
})
22+
}
23+
}
24+
25+
// Logf is like Log but supports formatting
26+
func Logf(ctx context.Context, level, format string, args ...interface{}) {
27+
message := fmt.Sprintf(format, args...)
28+
Log(ctx, level, message)
29+
}
30+
31+
// Convenience functions for common log levels
32+
func Notice(ctx context.Context, message string) {
33+
Log(ctx, "notice", message)
34+
}
35+
36+
func Noticef(ctx context.Context, format string, args ...interface{}) {
37+
Logf(ctx, "notice", format, args...)
38+
}
39+
40+
func Debug(ctx context.Context, message string) {
41+
Log(ctx, "debug", message)
42+
}
43+
44+
func Debugf(ctx context.Context, format string, args ...interface{}) {
45+
Logf(ctx, "debug", format, args...)
46+
}
47+
48+
func Error(ctx context.Context, message string) {
49+
Log(ctx, "error", message)
50+
}
51+
52+
func Errorf(ctx context.Context, format string, args ...interface{}) {
53+
Logf(ctx, "error", format, args...)
54+
}
55+
56+
func Warn(ctx context.Context, message string) {
57+
Log(ctx, "warn", message)
58+
}
59+
60+
func Warnf(ctx context.Context, format string, args ...interface{}) {
61+
Logf(ctx, "warn", format, args...)
62+
}

0 commit comments

Comments
 (0)