Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(analytics): Analytics Feature #2725

Merged
merged 43 commits into from
Feb 8, 2024
Merged

feat(analytics): Analytics Feature #2725

merged 43 commits into from
Feb 8, 2024

Conversation

yquansah
Copy link
Contributor

@yquansah yquansah commented Jan 30, 2024

This PR addresses the feature of mainly providing a uniform way of querying analytics through Flipt's API. We have been getting feature requests such as this for some time now, and our focus here is to provide a uniform contract that can be implemented by many different OLAP stores which then sits behind an API that the frontend can consume.

Main Features of this PR

  1. Provide configuration definitions for analytics store
  2. Provide analytics DDL for clickhouse in migrations directory
  3. Provide an analytics.proto definition for giving an API on top of an analytics store
  4. Integrate a clickhouse client for the first implementation of an analytics store
  5. UI for showing analytics on Flag page (on new analytics tab)

@erka
Copy link
Collaborator

erka commented Jan 30, 2024

This looks great!

I have one question. How will namespace_key be passed around? Is it part of flag_key?

Copy link
Collaborator

@markphelps markphelps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple more minor things.. looking great tho!

internal/cmd/http.go Outdated Show resolved Hide resolved
internal/server/analytics/clickhouse/client.go Outdated Show resolved Hide resolved
Copy link
Contributor

@GeorgeMac GeorgeMac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change is looking great so far 💪

Comment on lines 44 to 56
err := runMigrations(logger, connectionString)
if err != nil {
clickhouseErr = err
return
}

connection, err := connect(connectionString)
if err != nil {
clickhouseErr = err
return
}

conn = connection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably establish the connection once, instead of reconnecting after performing migrations:

Suggested change
err := runMigrations(logger, connectionString)
if err != nil {
clickhouseErr = err
return
}
connection, err := connect(connectionString)
if err != nil {
clickhouseErr = err
return
}
conn = connection
conn, clickhouseErr = connect(connectionString)
if clickhouseErr != nil {
return
}
clickhouseErr = runMigrations(logger, conn)
return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeorgeMac Got you. I was following the same pattern as the RDMS ones too but it does make sense to just use the connection once as you suggested.

@GeorgeMac GeorgeMac self-requested a review January 31, 2024 09:43
@GeorgeMac
Copy link
Contributor

GeorgeMac commented Jan 31, 2024

I wasn't paying attention and meant to make that a comment, not an approved review. But I do still approve of this PR 😂 so not wrong.

@yquansah yquansah changed the title feat(analytics): initial commit for querying analytics behind a contract with many possible implementations feat(analytics): Analytics Feature Feb 1, 2024
@erka
Copy link
Collaborator

erka commented Feb 1, 2024

@yquansah I know I am a bit late with this request. Is it possible to add reason and match to analytics table in clickhouse? My thoughts are that it's great to show evaluation chart but if key is disabled that doesn't provide much value to the end user and for us. Also it could be great to know proportion of match/ enabled for the flag. If it's only true or false may provide better insight as we may say it's a stale flag as there is no variation happen.

Copy link
Contributor

@GeorgeMac GeorgeMac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some suggestions around the changes to the migrator package and just a couple clean up comments. Change is looking great though.

internal/storage/sql/migrator.go Outdated Show resolved Hide resolved
internal/server/analytics/clickhouse/client.go Outdated Show resolved Hide resolved
internal/server/analytics/clickhouse/client.go Outdated Show resolved Hide resolved
@yquansah
Copy link
Contributor Author

yquansah commented Feb 1, 2024

@erka You are not late at all!! That is actually what is going to happen once I open it up. I plan to use https://clickhouse.com/docs/en/sql-reference/data-types/map to essentially have "tags" per measured metric.

Copy link
Contributor

@GeorgeMac GeorgeMac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exciting! Couple observations for the future and some suggestions / alterations I think could be helpful in here.

return clickhouse.Auth{
Username: c.Username,
Password: c.Password,
Database: "flipt_analytics",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yquansah just want to unresolve and go over this once more: Don't we need to make sure this is consistent with whatevere the database name is in the URL provided. Otherwise, if they try to provide their own name, this wont be valid. Maybe I am misunderstanding this option though.

step.intervalStep,
),
req.NamespaceKey,
req.FlagKey,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a cursory glance I don't think what you have here is exploitable, because you have validated your inputs already (parsed them as timestamps, or normalized them into intervals and so on), but format strings and SQL is usually a recipe for injection.

This is just to say, if you can coerce some of these parameters into the placeholder syntax like you have for namespace and flag key, you should be further protected from these kinds of vulnerability.
https://go.dev/doc/database/sql-injection

Comment on lines +33 to +34
//nolint:gosec
stmt := fmt.Sprintf("INSERT INTO %s VALUES %s", counterAnalyticsTable, strings.Join(valuePlaceHolders, ","))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a passing thought, nothing to really do for now.
TL;DR something to keep an eye on r.e. performance.

I imagine constantly rebuilding this query to be quite wasteful, particularly under load. I remember you saying their native client is more performant for inserts, so maybe we just don't sweat this until we eventually move to their other client. However, one way to mitigate this might be cache the insert query based on buffer size. To try and amortize the cost and avoid constantly reallocating this query.

I wonder if CH has a better way to prepare this query, so that the placeholders don't have to be repeated in this way too. That might be something their own client takes advantage of.

Aside: I looked at this first, because of the injection problem, and the nosec there made suspicious too. But I see you're building the placeholder query with this. Which itself is just a series of palceholders, no user inputs, so it is safe. Then you're passing the values via the placeholder on exec, which is good. So I can see why you step around the linting. (I am assuming the noline rule is upset here because of fmt stringing a SQL ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeorgeMac Yeah exactly! I tried to see if there was a better way to prepare this query. But you are right, we can look into that more native client.

I can place an issue in the project to do that. We can see if it has more features that is better suited for inserts.

internal/server/analytics/sink_test.go Outdated Show resolved Hide resolved
internal/server/middleware/grpc/middleware.go Outdated Show resolved Hide resolved
keyValue := []attribute.KeyValue{
{
Key: "flipt.evaluation.response",
Value: attribute.StringValue(string(evaluationResponsesBytes)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a bit of a mad hack: https://pkg.go.dev/go.opentelemetry.io/otel/attribute#Stringer

You could add a String() string method to *analytics.EvaluationResponse that marshals out a Go string. Then instead of marshalling this to JSON and then unmarshalling again in the exporter, you could pass it here with attributes.Stringer and just cast it back to a *analytics.EvaluationResponse in the exporter.
Otel will take care of marshalling lazilly when it needs to go over the wire.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeorgeMac Ahh I started to implement it this way just now and realized,

What is actually happening here is I am marshalling a []*EvaluationResponse to JSON instead of just a single *EvaluationResponse.

I chose to do it this way because it helps with the Batch implementation.

Copy link
Contributor

@GeorgeMac GeorgeMac Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you re-type that slice for the same ends?

type EvaluationResponses []*EvaluationResponse

func (r EvaluationResponses) String() string {
    v, _ := json.Marshal(r)
    return string(v)
}

And then use analytics.EvaluationResponses{} instead of []*analytics.EvaluationResponse?
Either way, not important, just seeing if we can avoid marshalling back and forth.

if err != nil {
return nil, err
}
if options.Auth.Database != "flipt_analytics" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we want to allow them to use whatever db name they choose

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markphelps We could do that. They would have to create that DB beforehand. I guess we can clearly state that in our docs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we try and parse the db name from the connection string?

if its there, then we use that, if not then we use this default?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we definitely want them to create it first themselves. Like we do with the relational DB.

}

return nil
},
}

cmd.Flags().StringVar(&providedConfigFile, "config", "", "path to config file")
cmd.Flags().StringVar(&database, "database", "regular", "string to denote which database type to migrate")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd.Flags().StringVar(&database, "database", "regular", "string to denote which database type to migrate")
cmd.Flags().StringVar(&database, "database", "default", "string to denote which database type to migrate")

Comment on lines 50 to 55
v.SetDefault("analytics", map[string]any{
"enabled": "false",
"clickhouse": map[string]any{
"enabled": "false",
"url": "",
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v.SetDefault("analytics", map[string]any{
"enabled": "false",
"clickhouse": map[string]any{
"enabled": "false",
"url": "",
},
v.SetDefault("analytics", map[string]any{
"storage.clickhouse": map[string]any{
"enabled": "false",
"url": "",
},

Copy link
Collaborator

@markphelps markphelps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple minor last minute comments, otherwise lgtm!

}

valuePlaceHolders := make([]string, 0, len(responses))
valueArgs := make([]interface{}, 0, len(responses)*9)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
valueArgs := make([]interface{}, 0, len(responses)*9)
valueArgs := make([]interface{}, 0, len(responses)*COLUMNS)

Could we make 9 a constant so its not a magic number

),
nowISO.getTimezoneOffset()
),
timeFormat
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to show the evaluation timestamps in the format that the user selects in preferences? ie UTC vs local?

CleanShot 2024-02-07 at 10 12 30

return {
timestamps: getFlagEvaluationCount.data?.timestamps,
values: getFlagEvaluationCount.data?.values,
unavailable: fetchError?.status === 501
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another option is we could check the /meta/config endpoint to see if analytics are enabled before making the call and depending on a 501

CleanShot 2024-02-07 at 10 18 46

I Could do this in a future PR though as it will require some redux work I think

@markphelps
Copy link
Collaborator

One other thing I noticed is that the schema_migrations table seems to be marking the migration as dirty after first migrate? I wonder if this is due to us potentially running migrate multiple times or something related to how golang migrate works?

I'm gonna check the sqlite db to see if the same thing happens there

CleanShot 2024-02-07 at 10 31 42

Copy link
Collaborator

@markphelps markphelps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one minor console.log removal, then ship it!

ui/src/components/graphs/index.tsx Outdated Show resolved Hide resolved
Copy link
Collaborator

@markphelps markphelps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📈

@yquansah yquansah merged commit 4613007 into main Feb 8, 2024
33 checks passed
@yquansah yquansah deleted the add-metrics branch February 8, 2024 18:04
erka added a commit to erka/flipt that referenced this pull request Feb 11, 2024
In flipt-io#2725 there were changes to z-index of header and Slideover and Modal
is behind the header. It's visible in creating new token/namespace or in
any modal dialog
kodiakhq bot pushed a commit that referenced this pull request Feb 11, 2024
In #2725 there were changes to z-index of header and Slideover and Modal
is behind the header. It's visible in creating new token/namespace or in
any modal dialog
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants