Skip to content

[Logs UI] State revamp for the log stream #65493

@afgomez

Description

@afgomez

Goals:

  • Understand and document all state for the log stream.
  • Split or group the state handling in units that accommodate the current code.
  • Establish a state management convention and use it everywhere in the log stream.
  • Potentially, reuse the same state management convention in other places of the infra app.

Considerations:

Log stream state

A container is a constate instance that holds state for a particular slice of the log stream app. The information that can be hold in state can be of one of the following types:

  • setting: A user-chosen value persisted between sessions.
  • data: some value to present in the UI. Might be user input, network response, etc.
  • UI: particular state about the interface. i.e.: an element is visible, a scroll is in a particular position.
  • network: some async state is happening that may or may not be represented in the UI. isLoading is network. isSpinnerVisible is UI.
  • other kitchen sink category. Could be internal state that needs to be exposed, could be a memoized value, or state composed from other piece of state

All containers live in plugins/infra/public/containers/logs.

This document will only show information-holding state, not their setters. Unless specified, you (the reader) can assume there is a setter for each entry. (i.e. textScale has a setTextScale function exposed as well).


LogSource
export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource);

Contains state about the current log source configuration

Takes:

  • sourceId The ID of the source
  • fetch fetcher function

Exposes:

  • sourceId (data) The ID of the source
  • derivedIndexPattern (data) - ???
  • isLoading (network)
  • hasFailedLoadingSource (network)
  • isLoadingSourceConfiguration (network)
  • sourceConfiguration (data)
  • hasFailedLoadingSourceStatus (network)
  • sourceStatus (data)
  • isLoadingSourceStatus (network)
  • isUninitialized (other)
  • loadSourceFailureMessage (data)
  • logIndicesExist (other)

LogViewConfiguration
export const LogViewConfiguration = createContainer(useLogViewConfiguration);

Provides state for the view settings.

Takes:

  • Nothing

Exposes:

  • textScale (setting) Size of the text: small, medium, large
  • textWrap (setting) should log lines wrap or not

LogFlyout
export const LogFlyout = createContainer(useLogFlyout);

Provides state for the view details flyout

Takes:

  • Nothing

Exposes:

  • flyoutVisible (UI): is the flyout visible
  • isLoading (network): the flyoutItem is being fetched or not.
  • flyoutId (data): ID of the log entry to show in the flyout.
  • surroundingLogsId (data): same ID. When set, the log stream highlights the relevant log line.
  • flyoutItem (data): fields for the log entry with the id flyoutId.

LogPositionState
export const LogPositionState = createContainer(useLogPositionState);

Provides information about the visible range of log entries and their position.

Takes:

  • Nothing

Exposes:

  • isInitialized (other): the hook has been initialized or not. If it's not, its state cannot be fully used yet.
  • targetPosition (data): Which log line should be centered in the UI
  • isStreaming (UI): Live stream is active or not.
  • firstVisiblePosition (UI) which log line is the first visible in the UI.
  • pagesBeforeStart (UI) how many scroll pages until the user reaches the top.
  • pagesAfterEnd (UI) how many scroll pages until the user reaches the bottom.
  • visibleMiddlePoint (UI) which log line is actually centered.
  • visibleMiddlePointTime (other), timestamp of the visibleMiddlePoint
  • visibleTimeInterval (UI) range between the time of the firstVisiblePosition and the last visible position (not exposed as state).
  • startDateExpression (data) beginning of date range (i.e now-1d)
  • endDateExpression (data) end of date range (i.e. now)
  • startTimestamp (other) resolved value for startDateExpression
  • endTimestamp (other) resolved value for endDateExpression
  • timestampsLastUpdate (other) when was the last time that the *DateExpressions were evaluated

ViewLogInContext
export const ViewLogInContext = createContainer(useViewLogInContext);

Holds state for the "View in context" modal

Takes:

  • sourceId from LogSource
  • startTimestamp from LogPositionState
  • endTimestamp from LogPositionState

Exposes:

  • contextEntry (data) the log entry whose context we need to fetch.
  • entries (data) log entries around contextEntry
  • isLoading (network) are the entries being fetched

LogFilterState
export const LogFilterState = createContainer(useLogFilterState);

Contains the user query in the search box.

Takes:

  • indexPattern from LogSource.derivedIndexPattern

Exposes:

  • filterQueryDraft (data) kuery expression the user has typed. Not yet commited.
  • filterQueryAsKuery (data) kuery expression the user has typed. Commited
  • filterQuery (other) serialized filterQueryAsKuery
  • isFilterQueryDraftValid (other) is the draft a valid kuery.

LogEntriesState
export const LogEntriesState = createContainer(useLogEntriesState);

The log entries themselves

Takes:

  • startTimestamp from LogPositionState
  • endTimestamp from LogPositionState
  • timestampsLastUpdate from LogPositionState
  • filterQuery from LogFilterState
  • timeKey from LogPositionState.targetPosition
  • pagesBeforeStart from LogPositionState
  • pagesAfterEnd from LogPositionState
  • sourceId from LogSource
  • isStreaming from LogPositionState
  • jumpToTargetPosition, callback from LogPositionState

Exposes:

  • entries (data) the log entries
  • topCursor (other) first loaded entry. Used for pagination
  • bottomCursor (other) last loaded entry. Used for pagination
  • centerCursor (other) middle point of all loaded entries
  • isReloading (network|_UI_) has the entries array being reset
  • isLoadingMore (network|_UI_) is fetching a new page.
  • lastLoadedTime (other) when did the last fetch happen
  • hasMoreBeforeStart (data) are there more entries before topCursor
  • hasMoreAfterEnd (data) are there more entries after bottomCursor

LogHighlightsState
export const LogHighlightsState = createContainer(useLogHighlightsState);

Holds state relevant to the highlight functionality.

Takes:

  • sourceId from LogSource
  • sourceVersion from LogSource.sourceConfiguration.version
  • entriesStart from LogEntriesState.topCursor
  • entriesEnd from LogEntriesState.bottomCursor
  • centerCursor from LogEntriesState.centerCursor
  • size from LogEntriesState.entries.length
  • filterQuery from LogFilter

Exposes:

  • highlightTerms (data) list of terms to be highlighted
  • logEntryHighlights (data) list of log entries with highlighted terms
  • logEntryHighlightsById (other) index of logEntryHighlights
  • logSummaryHighlights (data) list of date ranges with highlights on them, for the minimap
  • currentHighlightKey (UI) cursor of the log entry with the active highlight
  • hasPreviousHighlight (UI) are there any entries with highlights before the currentHighlightKey
  • hasNextHighlight (UI) are there any entries with highlights after the currentHighlightKey

Recommendations

reactive vs explicit data fetching
LogEntriesState triggers different data fetching based on changes on its props. These props include UI state, like scroll positions.

My advice is to change this. Instead of triggering a fetch as a reaction to input, it should expose callbacks that another component can use to trigger the fetch. This trigger can be made as a reaction to an input (i.e. a change in scroll position).

Evaluate if UI state should be held in containers at all. Since the state is only relevant to the UI, it should be moved to the UI components themselves.

For example, LogPositionState holds state about the user date range and the scroll position of the Log UI. The second category can be extracted to a view component instead of held in a container. The view component then can be responsible of passing it to LogEntriesState.

Unify how network state is represented. The possible states for any given request is always the same. We can use useTrackedPromise or a similar hook everywhere.

Revisit naming. Some concepts in the app have evolved or we understand better their functions. The names should reflect this.

  • LogFlyout => LogDetails: The fact that it uses a flyout is a UI decision and it should not be reflected in the container name.
  • position, TimeKey, cursor all represent the same idea. Pick one name
  • ...

Establish a convention for filenames and exports. A match between the main export and the file name is preferred. This makes it easy to find where the code is

  • log_flyout.tsx exports LogFlyout. This is good
  • log_entries/index.tsx exports LogEntriesState. We should either move the export to log_entries_state.tsx or rename the export to LogEntries.

Establish a convention to pass information between containers. Some containers get the information with useContext, whereas others pass the information through arguments.

Establish a convention for the internal container structure.
This has two goals:

  • When a programmer is working in the log streaming UI, they don't need to hold in their heads the intricacies of each particular container and can focus on the business logic.
  • When a new container needs to be created, the pattern to follow can be copied from an existing one.

The most common pattern seems to be the following:

/* container_name.tsx */

import createContainer from 'constate';
import "...";

interface ContainerNameProps {
  // Parameters to pass to the container
}

interface ContainerNameState {
  // public data
}

interface ContainerNameCallbacks {
  // public API of the container
}

function useContainerName(props: ContainerNameProps): ContainerNameState & ContainerNameCallbacks {

  // ...
  return { ... };
}

export const ContainerName = createContainer(useContainerName);

Metadata

Metadata

Assignees

Labels

Feature:Logs UILogs UI featureTeam:Infra Monitoring UI - DEPRECATEDDEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_servicesdiscusstechnical debtImprovement of the software architecture and operational architecturev8.0.0

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions