Skip to content

Commit 3e86bca

Browse files
authored
feat: create new telemetry handle that supports connection strings (#3729)
* feat: create new telemetry handle with connection strings * feat: Add application insights source code and remove dependency * feat: Create connection string helper function and update telemetry handle * feat: Polishing * feat: Address PR comments * feat: Update test telemetry handle initialization * feat: Re-initialise telemetry handles to fix race conditions during test execution * feat: revert string formatting changes * feat: fix lint
1 parent f2d2be5 commit 3e86bca

File tree

6 files changed

+239
-69
lines changed

6 files changed

+239
-69
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package aitelemetry
2+
3+
import (
4+
"strings"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
type connectionVars struct {
10+
instrumentationKey string
11+
ingestionURL string
12+
}
13+
14+
func (c *connectionVars) String() string {
15+
return "InstrumentationKey=" + c.instrumentationKey + ";IngestionEndpoint=" + c.ingestionURL
16+
}
17+
18+
func parseConnectionString(connectionString string) (*connectionVars, error) {
19+
connectionVars := &connectionVars{}
20+
21+
if connectionString == "" {
22+
return nil, errors.New("connection string cannot be empty")
23+
}
24+
25+
pairs := strings.Split(connectionString, ";")
26+
for _, pair := range pairs {
27+
kv := strings.SplitN(pair, "=", 2)
28+
if len(kv) != 2 {
29+
return nil, errors.Errorf("invalid connection string format: %s", pair)
30+
}
31+
key, value := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
32+
33+
if key == "" {
34+
return nil, errors.Errorf("key in connection string cannot be empty")
35+
}
36+
37+
switch strings.ToLower(key) {
38+
case "instrumentationkey":
39+
connectionVars.instrumentationKey = value
40+
case "ingestionendpoint":
41+
if value != "" {
42+
connectionVars.ingestionURL = value + "v2.1/track"
43+
}
44+
}
45+
}
46+
47+
if connectionVars.instrumentationKey == "" || connectionVars.ingestionURL == "" {
48+
return nil, errors.Errorf("missing required fields in connection string: %s", connectionVars)
49+
}
50+
51+
return connectionVars, nil
52+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package aitelemetry
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
const connectionString = "InstrumentationKey=0000-0000-0000-0000-0000;IngestionEndpoint=https://ingestion.endpoint.com/;LiveEndpoint=https://live.endpoint.com/;ApplicationId=1111-1111-1111-1111-1111"
10+
11+
func TestParseConnectionString(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
connectionString string
15+
want *connectionVars
16+
wantErr bool
17+
}{
18+
{
19+
name: "Valid connection string and instrumentation key",
20+
connectionString: connectionString,
21+
want: &connectionVars{
22+
instrumentationKey: "0000-0000-0000-0000-0000",
23+
ingestionURL: "https://ingestion.endpoint.com/v2.1/track",
24+
},
25+
wantErr: false,
26+
},
27+
{
28+
name: "Invalid connection string format",
29+
connectionString: "InvalidConnectionString",
30+
want: nil,
31+
wantErr: true,
32+
},
33+
{
34+
name: "Valid instrumentation key with missing ingestion endpoint",
35+
connectionString: "InstrumentationKey=0000-0000-0000-0000-0000;IngestionEndpoint=",
36+
want: nil,
37+
wantErr: true,
38+
},
39+
{
40+
name: "Missing instrumentation key with valid ingestion endpoint",
41+
connectionString: "InstrumentationKey=;IngestionEndpoint=https://ingestion.endpoint.com/",
42+
want: nil,
43+
wantErr: true,
44+
},
45+
{
46+
name: "Empty connection string",
47+
connectionString: "",
48+
want: nil,
49+
wantErr: true,
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
got, err := parseConnectionString(tt.connectionString)
56+
if tt.wantErr {
57+
require.Error(t, err, "Expected error but got none")
58+
} else {
59+
require.NoError(t, err, "Expected no error but got one")
60+
require.NotNil(t, got, "Expected a non-nil result")
61+
require.Equal(t, tt.want.instrumentationKey, got.instrumentationKey, "Instrumentation Key does not match")
62+
require.Equal(t, tt.want.ingestionURL, got.ingestionURL, "Ingestion URL does not match")
63+
}
64+
})
65+
}
66+
}

aitelemetry/telemetrywrapper.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/Azure/azure-container-networking/store"
1414
"github.com/microsoft/ApplicationInsights-Go/appinsights"
1515
"github.com/microsoft/ApplicationInsights-Go/appinsights/contracts"
16+
"github.com/pkg/errors"
1617
)
1718

1819
const (
@@ -161,7 +162,7 @@ func isPublicEnvironment(url string, retryCount, waitTimeInSecs int) (bool, erro
161162
return true, nil
162163
} else if err == nil {
163164
debugLog("[AppInsights] This is not azure public cloud:%s", cloudName)
164-
return false, fmt.Errorf("Not an azure public cloud: %s", cloudName)
165+
return false, errors.Errorf("not an azure public cloud: %s", cloudName)
165166
}
166167

167168
debugLog("GetAzureCloud returned err :%v", err)
@@ -214,6 +215,46 @@ func NewAITelemetry(
214215
return th, nil
215216
}
216217

218+
// NewWithConnectionString creates telemetry handle with user specified appinsights connection string.
219+
func NewWithConnectionString(connectionString string, aiConfig AIConfig) (TelemetryHandle, error) {
220+
debugMode = aiConfig.DebugMode
221+
222+
if connectionString == "" {
223+
debugLog("Empty connection string")
224+
return nil, errors.New("AI connection string is empty")
225+
}
226+
227+
setAIConfigDefaults(&aiConfig)
228+
229+
connectionVars, err := parseConnectionString(connectionString)
230+
if err != nil {
231+
debugLog("Error parsing connection string: %v", err)
232+
return nil, err
233+
}
234+
235+
telemetryConfig := appinsights.NewTelemetryConfiguration(connectionVars.instrumentationKey)
236+
telemetryConfig.EndpointUrl = connectionVars.ingestionURL
237+
telemetryConfig.MaxBatchSize = aiConfig.BatchSize
238+
telemetryConfig.MaxBatchInterval = time.Duration(aiConfig.BatchInterval) * time.Second
239+
240+
th := &telemetryHandle{
241+
client: appinsights.NewTelemetryClientFromConfig(telemetryConfig),
242+
appName: aiConfig.AppName,
243+
appVersion: aiConfig.AppVersion,
244+
diagListener: messageListener(),
245+
disableMetadataRefreshThread: aiConfig.DisableMetadataRefreshThread,
246+
refreshTimeout: aiConfig.RefreshTimeout,
247+
}
248+
249+
if th.disableMetadataRefreshThread {
250+
getMetadata(th)
251+
} else {
252+
go getMetadata(th)
253+
}
254+
255+
return th, nil
256+
}
257+
217258
// TrackLog function sends report (trace) to appinsights resource. It overrides few of the existing columns with app information
218259
// and for rest it uses custom dimesion
219260
func (th *telemetryHandle) TrackLog(report Report) {

aitelemetry/telemetrywrapper_test.go

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
var (
18-
th TelemetryHandle
18+
aiConfig AIConfig
1919
hostAgentUrl = "localhost:3501"
2020
getCloudResponse = "AzurePublicCloud"
2121
httpURL = "http://" + hostAgentUrl
@@ -54,6 +54,18 @@ func TestMain(m *testing.M) {
5454
return
5555
}
5656

57+
aiConfig = AIConfig{
58+
AppName: "testapp",
59+
AppVersion: "v1.0.26",
60+
BatchSize: 4096,
61+
BatchInterval: 2,
62+
RefreshTimeout: 10,
63+
GetEnvRetryCount: 1,
64+
GetEnvRetryWaitTimeInSecs: 2,
65+
DebugMode: true,
66+
DisableMetadataRefreshThread: true,
67+
}
68+
5769
exitCode := m.Run()
5870

5971
if runtime.GOOS == "linux" {
@@ -75,67 +87,78 @@ func handleGetCloud(w http.ResponseWriter, req *http.Request) {
7587
w.Write([]byte(getCloudResponse))
7688
}
7789

90+
func initTelemetry(_ *testing.T) (th1, th2 TelemetryHandle) {
91+
th1, err1 := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig)
92+
if err1 != nil {
93+
fmt.Printf("Error initializing AI telemetry: %v", err1)
94+
}
95+
96+
th2, err2 := NewWithConnectionString(connectionString, aiConfig)
97+
if err2 != nil {
98+
fmt.Printf("Error initializing AI telemetry with connection string: %v", err2)
99+
}
100+
101+
return
102+
}
103+
78104
func TestEmptyAIKey(t *testing.T) {
79105
var err error
80106

81-
aiConfig := AIConfig{
82-
AppName: "testapp",
83-
AppVersion: "v1.0.26",
84-
BatchSize: 4096,
85-
BatchInterval: 2,
86-
RefreshTimeout: 10,
87-
DebugMode: true,
88-
DisableMetadataRefreshThread: true,
89-
}
90107
_, err = NewAITelemetry(httpURL, "", aiConfig)
91108
if err == nil {
92-
t.Errorf("Error intializing AI telemetry:%v", err)
109+
t.Errorf("Error initializing AI telemetry:%v", err)
110+
}
111+
112+
_, err = NewWithConnectionString("", aiConfig)
113+
if err == nil {
114+
t.Errorf("Error initializing AI telemetry with connection string:%v", err)
93115
}
94116
}
95117

96118
func TestNewAITelemetry(t *testing.T) {
97119
var err error
98120

99-
aiConfig := AIConfig{
100-
AppName: "testapp",
101-
AppVersion: "v1.0.26",
102-
BatchSize: 4096,
103-
BatchInterval: 2,
104-
RefreshTimeout: 10,
105-
GetEnvRetryCount: 1,
106-
GetEnvRetryWaitTimeInSecs: 2,
107-
DebugMode: true,
108-
DisableMetadataRefreshThread: true,
121+
th1, th2 := initTelemetry(t)
122+
if th1 == nil {
123+
t.Errorf("Error initializing AI telemetry: %v", err)
109124
}
110-
th, err = NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig)
111-
if th == nil {
112-
t.Errorf("Error intializing AI telemetry: %v", err)
125+
126+
if th2 == nil {
127+
t.Errorf("Error initializing AI telemetry with connection string: %v", err)
113128
}
114129
}
115130

116131
func TestTrackMetric(t *testing.T) {
132+
th1, th2 := initTelemetry(t)
133+
117134
metric := Metric{
118135
Name: "test",
119136
Value: 1.0,
120137
CustomDimensions: make(map[string]string),
121138
}
122139

123140
metric.CustomDimensions["dim1"] = "col1"
124-
th.TrackMetric(metric)
141+
th1.TrackMetric(metric)
142+
th2.TrackMetric(metric)
125143
}
126144

127145
func TestTrackLog(t *testing.T) {
146+
th1, th2 := initTelemetry(t)
147+
128148
report := Report{
129149
Message: "test",
130150
Context: "10a",
131151
CustomDimensions: make(map[string]string),
132152
}
133153

134154
report.CustomDimensions["dim1"] = "col1"
135-
th.TrackLog(report)
155+
th1.TrackLog(report)
156+
th2.TrackLog(report)
136157
}
137158

138159
func TestTrackEvent(t *testing.T) {
160+
th1, th2 := initTelemetry(t)
161+
139162
event := Event{
140163
EventName: "testEvent",
141164
ResourceID: "SomeResourceId",
@@ -144,35 +167,20 @@ func TestTrackEvent(t *testing.T) {
144167

145168
event.Properties["P1"] = "V1"
146169
event.Properties["P2"] = "V2"
147-
th.TrackEvent(event)
170+
th1.TrackEvent(event)
171+
th2.TrackEvent(event)
148172
}
149173

150174
func TestFlush(t *testing.T) {
151-
th.Flush()
152-
}
175+
th1, th2 := initTelemetry(t)
153176

154-
func TestClose(t *testing.T) {
155-
th.Close(10)
177+
th1.Flush()
178+
th2.Flush()
156179
}
157180

158-
func TestClosewithoutSend(t *testing.T) {
159-
var err error
160-
161-
aiConfig := AIConfig{
162-
AppName: "testapp",
163-
AppVersion: "v1.0.26",
164-
BatchSize: 4096,
165-
BatchInterval: 2,
166-
DisableMetadataRefreshThread: true,
167-
RefreshTimeout: 10,
168-
GetEnvRetryCount: 1,
169-
GetEnvRetryWaitTimeInSecs: 2,
170-
}
171-
172-
thtest, err := NewAITelemetry(httpURL, "00ca2a73-c8d6-4929-a0c2-cf84545ec225", aiConfig)
173-
if thtest == nil {
174-
t.Errorf("Error intializing AI telemetry:%v", err)
175-
}
181+
func TestClose(t *testing.T) {
182+
th1, th2 := initTelemetry(t)
176183

177-
thtest.Close(10)
184+
th1.Close(10)
185+
th2.Close(10)
178186
}

0 commit comments

Comments
 (0)