hypertrace / hypertrace-ui

UI for Hypertrace
Other
25 stars 12 forks source link

Attribute based drill down should automatically come to Trace or Span tab on explorer as per attribute scope #1250

Open JBAhire opened 3 years ago

JBAhire commented 3 years ago

Use Case

Current implementation of attribute based drill down always redirects to Trace tab of explorer with tag filter applied. But as some of the attributes don't exist in trace scope it shows no data. We should check the scope of attribute and then redirect accordingly.

Proposal

As there will always be data in span context, it make more sense to always go to Spans tab in that case.

Questions to address (if any)

jaywalker21 commented 2 years ago

I have started looking into this. Following is the information I could gather around the changes we might need for the implementation to go through. Currently the following attributes are sent as Trace related attributes.

[
    {
        "name": "startTime",
        "displayName": "Start Time",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "duration",
        "displayName": "Duration",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "ms",
        "onlySupportsAggregation": true,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG",
            "AVGRATE",
            "PERCENTILE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiTraceErrorSpanCount",
        "displayName": "Api Trace Error Span Count",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "protocol",
        "displayName": "Protocol",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "statusCode",
        "displayName": "Status Code",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "requestMethod",
        "displayName": "HTTP Method",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "tags",
        "displayName": "Tags",
        "type": "STRING_MAP",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiDiscoveryState",
        "displayName": "Endpoint Discovery State",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiExitCalls",
        "displayName": "Api Exit Calls",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "statusMessage",
        "displayName": "Status Message",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "userAgent",
        "displayName": "User Agent",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spaceIds",
        "displayName": "Space IDs",
        "type": "STRING_ARRAY",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "calls",
        "displayName": "Calls",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": true,
        "supportedAggregations": [
            "COUNT",
            "AVGRATE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiBoundaryType",
        "displayName": "Endpoint Boundary Type",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "errorCount",
        "displayName": "Error Count",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "Errors",
        "onlySupportsAggregation": true,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG",
            "AVGRATE",
            "PERCENTILE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "status",
        "displayName": "Status",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiName",
        "displayName": "Endpoint Name",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiCalleeNameCount",
        "displayName": "Api Callee Name Count",
        "type": "STRING_MAP",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "requestUrl",
        "displayName": "URL",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "traceId",
        "displayName": "Trace ID",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "endTime",
        "displayName": "End Time",
        "type": "LONG",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiTraceId",
        "displayName": "Endpoint Trace ID",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "serviceName",
        "displayName": "Service Name",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiId",
        "displayName": "Endpoint ID",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "serviceId",
        "displayName": "Service ID",
        "type": "STRING",
        "scope": "API_TRACE",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    }
]

And the following are Span related attributes.

[
    {
        "name": "id",
        "displayName": "Span ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "type",
        "displayName": "Span Type",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "statusCode",
        "displayName": "Status Code",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spaceIds",
        "displayName": "Space IDs",
        "type": "STRING_ARRAY",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "traceId",
        "displayName": "Trace ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "serviceName",
        "displayName": "Service Name",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "duration",
        "displayName": "Duration",
        "type": "LONG",
        "scope": "SPAN",
        "units": "ms",
        "onlySupportsAggregation": true,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG",
            "AVGRATE",
            "PERCENTILE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "parentSpanId",
        "displayName": "Parent Span ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "displaySpanName",
        "displayName": "Span Name",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "startTime",
        "displayName": "Start Time",
        "type": "LONG",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "endTime",
        "displayName": "End Time",
        "type": "LONG",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spanTags",
        "displayName": "Tags",
        "type": "STRING_MAP",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiName",
        "displayName": "Endpoint Name",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiId",
        "displayName": "Endpoint ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "exceptionCount",
        "displayName": "Exception Count",
        "type": "LONG",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": true,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG",
            "AVGRATE",
            "PERCENTILE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "displayEntityName",
        "displayName": "Entity Name",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spanRequestMethod",
        "displayName": "HTTP Method",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spanRequestUrl",
        "displayName": "URL",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "apiTraceId",
        "displayName": "Endpoint Trace ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "errorCount",
        "displayName": "Error Count",
        "type": "LONG",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": true,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "SUM",
            "MIN",
            "MAX",
            "AVG",
            "AVGRATE",
            "PERCENTILE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "protocolName",
        "displayName": "Protocol",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "serviceId",
        "displayName": "Service ID",
        "type": "STRING",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [
            "DISTINCTCOUNT"
        ],
        "groupable": true,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spans",
        "displayName": "Spans",
        "type": "LONG",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": true,
        "supportedAggregations": [
            "COUNT",
            "AVGRATE"
        ],
        "groupable": false,
        "__typename": "AttributeMetadata"
    },
    {
        "name": "spanRequestParams",
        "displayName": "Request Params",
        "type": "STRING_MAP",
        "scope": "SPAN",
        "units": "",
        "onlySupportsAggregation": false,
        "onlySupportsGrouping": false,
        "supportedAggregations": [],
        "groupable": false,
        "__typename": "AttributeMetadata"
    }
]

The logic for explorer dashboard as well as navigation on click of the filter is based upon the same attributes. Now, because the names of the attribute are difference for span and trace tab in the explorer, the filter with a different name doesn't retain it's value which is causing https://github.com/hypertrace/hypertrace-ui/issues/1249. If the service responsible for sending the attributes can ensure that the same names are used for the filters used in both span view and trace view in explorer view and everywhere else, both of these issues will be solved.

One example for the difference in name.

For Trace tab, we call HTTP Method as requestMethod, but for Span tab, we call it spanRequestMethod in the filter.

@aaron-steinfeld @anandtiwary @itssharmasandeep @jyothishjose6190

Wanted to know your thoughts on the same.

aaron-steinfeld commented 2 years ago

The logic for explorer dashboard as well as navigation on click of the filter is based upon the same attributes.

I think this is the fundamental misunderstanding. The attributes aren't the same. There is some overlap, and there may be a few attributes which overlap exactly in meaning but have different names (these are defined via config in https://github.com/hypertrace/config-bootstrapper/tree/main/config-bootstrapper/src/main/resources/configs/config-bootstrapper/attribute-service), but changing these names directly isn't really feasible since it would break existing queries (we could do some extra effort to make it compatible, but not sure it's worth it). Other attributes in both scopes are just exclusive to one scope or the other, so we should never operate under the assumption that a mapping can always be made.

So as far as #1250 is concerned, we should be drilling into the scope we started from. If we're viewing a span attribute, we should be going to the span explorer; if viewing a trace attribute, trace explorer. No attempt at mapping should be done here (nor is necessary). For #1249 , we should be doing a best effort and mapping where there's an exact match (I thought this was the existing behavior, but would have to check). If there are particular attributes of concern that have a counterpart with a different name, let's enumerate those and see what our options are to align them (the two that come to mind are either adding an alias on the backend, or maintaining a dictionary on the UI - neither is great).

jaywalker21 commented 2 years ago

Hey @aaron-steinfeld , I did a comparison for all the attributes and came up with the below table.

Field Information

Field Trace Filter Name Span Filter Name
Span ID - id
Span Name - displaySpanName
Span Type - type
Parent Span ID - parentSpanId
Exception Count - exceptionCount
Entity Name - displayEntityName
Request Params - spanRequestParams
HTTP Method requestMethod spanRequestMethod
Protocol protocol protocolName
URL requestURL spanRequestUrl
Tags tags spanTags
Status Code statusCode statusCode
Space IDs spaceIds spaceIds
Service ID serviceId serviceId
Trace ID traceId traceId
Service Name serviceName serviceName
Duration duration duration
Start Time startTime startTime
End Time endTime endTime
Error Count errorCount errorCount
Endpoint Name apiName apiName
Endpoint ID apiId apiId
Endpoint Trace ID apiTraceId apiTraceId
Api Trace Error Span Count apiTraceErrorSpanCount -
Endpoint Discovery State apiDiscoveryState -
Endpoint Boundary Type apiBoundaryType -
Api Callee Name Count apiCalleeNameCount -
Api Exit Calls apiExitCalls -
Status Message statusMessage -
User Agent userAgent -
Calls calls -
Status status -

As of now I could find 4 fields which have different names - HTTP Method, Protocol, URL, Tags.

The table above gives information about both mutually exclusive fields, as well as fields with different name which is present for both views. I think the next question is about how to approach the same? Given the number of fields with different name (4), we can choose to update the id of such fields from service side, or send along some extra metadata along with each field which helps the UI to do the conversion on the fly. Like an additional field like alternateId or something of the kind. The UI might need to do some rework on the response to compute the object.

aaron-steinfeld commented 2 years ago

Thanks for putting together the list @jaywalker21 . So the options I see and their pros/cons:

  1. Keep a map in the UI to support the conversion for those 4 fields. Very easy to implement and it's nice to keep this contained to the UI code since that's the only place that cares about this relationship. Downside here is that the definition of these attributes is config driven (in the bootstrapper config), so we're depending on certain config values. That's not altogether different than what we do in the UI in other places with hardcoded queries to power dashboards, but seems just a hair worse.
  2. Change the attribute keys of the mismatched attributes to match the opposite scope. This is ideal, like it never happened, but can potentially lead to unintended consequences both for fresh (in terms of hardcoded queries, which would need to be searched for) and upgrade installs (where users may be using the old names and now be broken).
  3. Add alternate alias attributes for each pair that do match. This solves the problem from 2 in a way, but creates its own problems - we're left with two names for the four attributes in perpetuity, and when selecting in the list we'd want to dedupe so only one name shows up. We might be able to use the internal flag to hide the old one (I think so but would need to dig in further), which should maintain support for hardcoded queries. Would have to look into that, but if that doesn't work wouldn't want to introduce a new piece of metadata for this. Even if it does work neatly, I'm not convinced it's worth the config complexity of having both.
  4. Add some other piece of metadata to the attribute definition to support the mapping.

Given the options, I think I'd suggest the first approach. It's the quickest to implement - a single PR likely under 50 lines, and how the smallest surface area since nothing is downstream of the change. If we need to unwind it for whatever reason, trivial to do so. What do you think?

jaywalker21 commented 2 years ago

Hi @aaron-steinfeld, Below are my thoughts around each of the options

Option 1

Will be quite easy to do from implementation perspective. Just feels a little wrong from design perspective given how everything is designed (config driven). But, we can take this approach for now and think around moving any meta info/config to backend in the future if a need arises. The only downside, as you mentioned is the hardcoded values that the code will maintain.

Option 2

Is there any way to check for the internal references of the query/URL? From the user base perspective, if there is a lot of reuse/bookmarking of the URLs, then this approach isn't feasible now.

Option 3

I think it's a lot of overhead to maintain and carry out on the browser. For every filter being applied, it might require an iteration, which again isn't ideal from performance perspective too. It will also considerably reduce the readability of the code.

Option 4

This option is very close to Option 2 and will not want to move ahead with this for the same reasons.

I think it would be better to go with Option 1 for now. We can revisit if need be in the future, but this looks the most suitable approach for the time being.