Skip to content

Commit

Permalink
Add collection of SnippetsFilter Snippet directive, context, and tota…
Browse files Browse the repository at this point in the history
…l count
  • Loading branch information
bjee19 committed Oct 10, 2024
1 parent ee3a4fd commit b805ed5
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 13 deletions.
131 changes: 129 additions & 2 deletions internal/mode/static/telemetry/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"runtime"
"sort"
"strings"

tel "github.com/nginxinc/telemetry-exporter/pkg/telemetry"
appsv1 "k8s.io/api/apps/v1"
Expand All @@ -14,6 +16,7 @@ import (
k8sversion "k8s.io/apimachinery/pkg/util/version"
"sigs.k8s.io/controller-runtime/pkg/client"

ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/config"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
Expand Down Expand Up @@ -46,8 +49,15 @@ type Data struct {
// FlagValues contains the values of the command-line flags, where each value corresponds to the flag from FlagNames
// at the same index.
// Each value is either 'true' or 'false' for boolean flags and 'default' or 'user-defined' for non-boolean flags.
FlagValues []string
NGFResourceCounts // embedding is required by the generator.
FlagValues []string
// SnippetsFiltersContextDirectives contains the context-directive strings of all applied SnippetsFilters.
// Both lists are ordered first by count, then by lexicographical order on the context-directive string.
SnippetsFiltersContextDirectives []string
// SnippetsFiltersContextDirectivesCount contains the count of the context-directive strings, where each count
// corresponds to the string from SnippetsFiltersContextDirectives at the same index. Both lists are ordered
// first by count, then by lexicographical order on the context-directive string.
SnippetsFiltersContextDirectivesCount []int64
NGFResourceCounts // embedding is required by the generator.
// NGFReplicaCount is the number of replicas of the NGF Pod.
NGFReplicaCount int64
}
Expand Down Expand Up @@ -146,6 +156,13 @@ func (c DataCollectorImpl) Collect(ctx context.Context) (Data, error) {
return Data{}, fmt.Errorf("failed to get NGF deploymentID: %w", err)
}

snippetsFiltersContextDirectives,
snippetsFiltersContextDirectivesCount,
err := collectSnippetsFilterSnippetsInfo(c.cfg.GraphGetter)
if err != nil {
return Data{}, fmt.Errorf("failed to collect snippet filter directive info: %w", err)
}

data := Data{
Data: tel.Data{
ProjectName: "NGF",
Expand All @@ -162,6 +179,9 @@ func (c DataCollectorImpl) Collect(ctx context.Context) (Data, error) {
FlagNames: c.cfg.Flags.Names,
FlagValues: c.cfg.Flags.Values,
NGFReplicaCount: int64(replicaCount),
// maybe SnippetValues?
SnippetsFiltersContextDirectives: snippetsFiltersContextDirectives,
SnippetsFiltersContextDirectivesCount: snippetsFiltersContextDirectivesCount,
}

return data, nil
Expand Down Expand Up @@ -382,3 +402,110 @@ func collectClusterInformation(ctx context.Context, k8sClient client.Reader) (cl

return clusterInfo, nil
}

type sfContextDirective struct {
context string
directive string
}

func collectSnippetsFilterSnippetsInfo(graphGetter GraphGetter) ([]string, []int64, error) {
g := graphGetter.GetLatestGraph()
if g == nil {
return nil, nil, errors.New("latest graph cannot be nil")
}

contextDirectiveMap := make(map[sfContextDirective]int)

for name := range g.SnippetsFilters {
sf := g.SnippetsFilters[name]
if sf == nil {
continue
}

for nginxContext := range sf.Snippets {
snippetValue := sf.Snippets[nginxContext]

var parsedContext string
switch nginxContext {
case ngfAPI.NginxContextMain:
parsedContext = "main"
case ngfAPI.NginxContextHTTP:
parsedContext = "http"
case ngfAPI.NginxContextHTTPServer:
parsedContext = "server"
case ngfAPI.NginxContextHTTPServerLocation:
parsedContext = "location"
default:
parsedContext = "unknown"
}

directives := parseSnippetValueIntoDirectives(snippetValue)

for _, directive := range directives {
contextDirective := sfContextDirective{
context: parsedContext,
directive: directive,
}

contextDirectiveMap[contextDirective]++
}
}
}

contextDirectiveList, countList := parseContextDirectiveMapIntoLists(contextDirectiveMap)

return contextDirectiveList, countList, nil
}

func parseSnippetValueIntoDirectives(snippetValue string) []string {
separatedDirectives := strings.Split(snippetValue, ";")
directives := make([]string, 0, len(separatedDirectives))

for _, directive := range separatedDirectives {
// the strings.TrimSpace is needed in the case of multi-line NGINX Snippet values
directive = strings.Split(strings.TrimSpace(directive), " ")[0]

// splitting on the delimiting character can result in a directive being empty or a space/newline character,
// so we check here to ensure it's not
if directive != "" {
directives = append(directives, directive)
}
}

return directives
}

// parseContextDirectiveMapIntoLists returns two same-length lists where the elements at each corresponding index
// are paired together.
// The first list contains strings which are the NGINX context and directive of a Snippet joined with a hyphen.
// The second list contains ints which are the count of total same context-directive values of the first list.
// Both lists are ordered based off of count first, then lexicographically on the context-directive string.
func parseContextDirectiveMapIntoLists(contextDirectiveMap map[sfContextDirective]int) ([]string, []int64) {
type sfContextDirectiveCount struct {
contextDirective string
count int64
}

kvPairs := make([]sfContextDirectiveCount, 0, len(contextDirectiveMap))

for k, v := range contextDirectiveMap {
kvPairs = append(kvPairs, sfContextDirectiveCount{k.context + "-" + k.directive, int64(v)})
}

sort.Slice(kvPairs, func(i, j int) bool {
if kvPairs[i].count == kvPairs[j].count {
return kvPairs[i].contextDirective < kvPairs[j].contextDirective
}
return kvPairs[i].count > kvPairs[j].count
})

contextDirectiveList := make([]string, len(kvPairs))
countList := make([]int64, len(kvPairs))

for i, pair := range kvPairs {
contextDirectiveList[i] = pair.contextDirective
countList[i] = pair.count
}

return contextDirectiveList, countList
}
67 changes: 58 additions & 9 deletions internal/mode/static/telemetry/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1"
"github.com/nginxinc/nginx-gateway-fabric/internal/framework/events/eventsfakes"
"github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/config"
Expand Down Expand Up @@ -168,11 +169,13 @@ var _ = Describe("Collector", Ordered, func() {
InstallationID: string(ngfReplicaSet.ObjectMeta.OwnerReferences[0].UID),
ClusterNodeCount: 1,
},
NGFResourceCounts: telemetry.NGFResourceCounts{},
NGFReplicaCount: 1,
ImageSource: "local",
FlagNames: flags.Names,
FlagValues: flags.Values,
NGFResourceCounts: telemetry.NGFResourceCounts{},
NGFReplicaCount: 1,
ImageSource: "local",
FlagNames: flags.Names,
FlagValues: flags.Values,
SnippetsFiltersContextDirectives: []string{},
SnippetsFiltersContextDirectivesCount: []int64{},
}

k8sClientReader = &eventsfakes.FakeReader{}
Expand Down Expand Up @@ -329,8 +332,26 @@ var _ = Describe("Collector", Ordered, func() {
},
NginxProxy: &graph.NginxProxy{},
SnippetsFilters: map[types.NamespacedName]*graph.SnippetsFilter{
{Namespace: "test", Name: "sf-1"}: {},
{Namespace: "test", Name: "sf-2"}: {},
{Namespace: "test", Name: "sf-1"}: {
Snippets: map[ngfAPI.NginxContext]string{
ngfAPI.NginxContextMain: "worker_priority 0;",
ngfAPI.NginxContextHTTP: "aio on;",
ngfAPI.NginxContextHTTPServer: "auth_delay 10s;",
ngfAPI.NginxContextHTTPServerLocation: "keepalive_time 10s;",
},
},
{Namespace: "test", Name: "sf-2"}: {
Snippets: map[ngfAPI.NginxContext]string{
// String representation of multi-line yaml value using > character
ngfAPI.NginxContextMain: "worker_priority 1; worker_rlimit_nofile 50;\n",
// String representation of NGINX values on same line
ngfAPI.NginxContextHTTP: "aio off; client_body_timeout 70s;",
// String representation of multi-line yaml using no special character besides a new line
ngfAPI.NginxContextHTTPServer: "auth_delay 100s; ignore_invalid_headers off;",
// String representation of multi-line yaml value using | character
ngfAPI.NginxContextHTTPServerLocation: "keepalive_time 100s;\nallow 10.0.0.0/8;\n",
},
},
{Namespace: "test", Name: "sf-3"}: {},
},
}
Expand Down Expand Up @@ -389,10 +410,38 @@ var _ = Describe("Collector", Ordered, func() {
expData.ClusterVersion = "1.29.2"
expData.ClusterPlatform = "kind"

data, err := dataCollector.Collect(ctx)
expData.SnippetsFiltersContextDirectives = []string{
"http-aio",
"location-keepalive_time",
"main-worker_priority",
"server-auth_delay",
"http-client_body_timeout",
"location-allow",
"main-worker_rlimit_nofile",
"server-ignore_invalid_headers",
}
expData.SnippetsFiltersContextDirectivesCount = []int64{
2,
2,
2,
2,
1,
1,
1,
1,
}

data, err := dataCollector.Collect(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(expData).To(Equal(data))

Expect(data.Data).To(Equal(expData.Data))
Expect(data.NGFResourceCounts).To(Equal(expData.NGFResourceCounts))
Expect(data.ImageSource).To(Equal(expData.ImageSource))
Expect(data.FlagNames).To(Equal(expData.FlagNames))
Expect(data.FlagValues).To(Equal(expData.FlagValues))
Expect(data.NGFReplicaCount).To(Equal(expData.NGFReplicaCount))
Expect(data.SnippetsFiltersContextDirectives).To(Equal(expData.SnippetsFiltersContextDirectives))
Expect(data.SnippetsFiltersContextDirectivesCount).To(Equal(expData.SnippetsFiltersContextDirectivesCount))
})
})
})
Expand Down
9 changes: 9 additions & 0 deletions internal/mode/static/telemetry/data.avdl
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,14 @@ attached at the Gateway level. */
/** NGFReplicaCount is the number of replicas of the NGF Pod. */
long? NGFReplicaCount = null;

/** SnippetsFiltersContextDirectives contains the context-directive strings of all applied SnippetsFilters.
Both lists are ordered first by count, then by lexicographical order on the context-directive string. */
union {null, array<string>} SnippetsFiltersContextDirectives = null;

/** SnippetsFiltersContextDirectivesCount contains the count of the context-directive strings, where each count
corresponds to the string from SnippetsFiltersContextDirectives at the same index. Both lists are ordered
first by count, then by lexicographical order on the context-directive string. */
union {null, array<long>} SnippetsFiltersContextDirectivesCount = null;

}
}
2 changes: 2 additions & 0 deletions internal/mode/static/telemetry/data_attributes_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func (d *Data) Attributes() []attribute.KeyValue {
attrs = append(attrs, attribute.StringSlice("FlagValues", d.FlagValues))
attrs = append(attrs, d.NGFResourceCounts.Attributes()...)
attrs = append(attrs, attribute.Int64("NGFReplicaCount", d.NGFReplicaCount))
attrs = append(attrs, attribute.StringSlice("SnippetsFiltersContextDirectives", d.SnippetsFiltersContextDirectives))
attrs = append(attrs, attribute.Int64Slice("SnippetsFiltersContextDirectivesCount", d.SnippetsFiltersContextDirectivesCount))

return attrs
}
Expand Down
11 changes: 10 additions & 1 deletion internal/mode/static/telemetry/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ func TestDataAttributes(t *testing.T) {
NginxProxyCount: 12,
SnippetsFilterCount: 13,
},
NGFReplicaCount: 3,
NGFReplicaCount: 3,
SnippetsFiltersContextDirectives: []string{"main-three-count", "http-two-count", "server-one-count"},
SnippetsFiltersContextDirectivesCount: []int64{3, 2, 1},
}

expected := []attribute.KeyValue{
Expand Down Expand Up @@ -71,6 +73,11 @@ func TestDataAttributes(t *testing.T) {
attribute.Int64("NginxProxyCount", 12),
attribute.Int64("SnippetsFilterCount", 13),
attribute.Int64("NGFReplicaCount", 3),
attribute.StringSlice(
"SnippetsFiltersContextDirectives",
[]string{"main-three-count", "http-two-count", "server-one-count"},
),
attribute.IntSlice("SnippetsFiltersContextDirectivesCount", []int{3, 2, 1}),
}

result := data.Attributes()
Expand Down Expand Up @@ -111,6 +118,8 @@ func TestDataAttributesWithEmptyData(t *testing.T) {
attribute.Int64("NginxProxyCount", 0),
attribute.Int64("SnippetsFilterCount", 0),
attribute.Int64("NGFReplicaCount", 0),
attribute.StringSlice("SnippetsFiltersContextDirectives", nil),
attribute.IntSlice("SnippetsFiltersContextDirectivesCount", nil),
}

result := data.Attributes()
Expand Down
2 changes: 1 addition & 1 deletion site/content/overview/product-telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Telemetry data is collected once every 24 hours and sent to a service managed by
- **Image Build Source:** whether the image was built by GitHub or locally (values are `gha`, `local`, or `unknown`). The source repository of the images is **not** collected.
- **Deployment Flags:** a list of NGINX Gateway Fabric Deployment flags that are specified by a user. The actual values of non-boolean flags are **not** collected; we only record that they are either `true` or `false` for boolean flags and `default` or `user-defined` for the rest.
- **Count of Resources:** the total count of resources related to NGINX Gateway Fabric. This includes `GatewayClasses`, `Gateways`, `HTTPRoutes`,`GRPCRoutes`, `TLSRoutes`, `Secrets`, `Services`, `BackendTLSPolicies`, `ClientSettingsPolicies`, `NginxProxies`, `ObservabilityPolicies`, `SnippetsFilters`, and `Endpoints`. The data within these resources is **not** collected.

- **SnippetsFilters Info**a list of context-directive strings from applied SnippetFilters and a total count per strings. The actual value of any NGINX directive is **not** collected.
This data is used to identify the following information:

- The flavors of Kubernetes environments that are most popular among our users.
Expand Down
2 changes: 2 additions & 0 deletions tests/suite/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ var _ = Describe("Telemetry test with OTel collector", Label("telemetry"), func(
"NginxProxyCount: Int(0)",
"SnippetsFilterCount: Int(0)",
"NGFReplicaCount: Int(1)",
"SnippetsFiltersContextDirectives: Slice",
"SnippetsFiltersContextDirectivesCount: Slice",
},
)
})
Expand Down

0 comments on commit b805ed5

Please sign in to comment.