-
Notifications
You must be signed in to change notification settings - Fork 273
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add event and log streaming
* Added an event bus to Logger, which emits events when log entries are created or updated. * Added the `BufferedEventStream` class. This is used for batching and streaming events from the Logger and the active Garden instance to the platform when the user is logged in. Later, we can use this class for streaming to the dashboard as well.
- Loading branch information
Showing
18 changed files
with
379 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* | ||
* Copyright (C) 2018-2020 Garden Technologies, Inc. <[email protected]> | ||
* | ||
* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
*/ | ||
|
||
import { registerCleanupFunction } from "../util/util" | ||
import { Events, EventName, EventBus, eventNames } from "../events" | ||
import { LogEntryMetadata, LogEntry } from "../logger/log-entry" | ||
import { chainMessages } from "../logger/renderers" | ||
import { got } from "../util/http" | ||
import { makeAuthHeader } from "./auth" | ||
|
||
export type StreamEvent = { | ||
name: EventName | ||
payload: Events[EventName] | ||
timestamp: Date | ||
} | ||
|
||
export interface LogEntryEvent { | ||
key: string | ||
parentKey: string | null | ||
revision: number | ||
msg: string | string[] | ||
timestamp: Date | ||
data?: any | ||
section?: string | ||
metadata?: LogEntryMetadata | ||
} | ||
|
||
export function formatForEventStream(entry: LogEntry): LogEntryEvent { | ||
const { section, data } = entry.getMessageState() | ||
const { key, revision } = entry | ||
const parentKey = entry.parent ? entry.parent.key : null | ||
const metadata = entry.getMetadata() | ||
const msg = chainMessages(entry.getMessageStates() || []) | ||
const timestamp = new Date() | ||
return { key, parentKey, revision, msg, data, metadata, section, timestamp } | ||
} | ||
|
||
export const FLUSH_INTERVAL_MSEC = 1000 | ||
export const MAX_BATCH_SIZE = 100 | ||
|
||
/** | ||
* Buffers events and log entries and periodically POSTs them to the platform. | ||
* | ||
* Subscribes to logger events once, in the constructor. | ||
* | ||
* Subscribes to Garden events via the connect method, since we need to subscribe to the event bus of | ||
* new Garden instances (and unsubscribe from events from the previously connected Garden instance, if | ||
* any) e.g. when config changes during a watch-mode command. | ||
*/ | ||
export class BufferedEventStream { | ||
private log: LogEntry | ||
private eventBus: EventBus | ||
public sessionId: string | ||
private platformUrl: string | ||
private clientAuthToken: string | ||
private projectName: string | ||
|
||
/** | ||
* We maintain this map to facilitate unsubscribing from a previously connected event bus | ||
* when a new event bus is connected. | ||
*/ | ||
private gardenEventListeners: { [eventName: string]: (payload: any) => void } | ||
|
||
private intervalId: NodeJS.Timer | null | ||
private bufferedEvents: StreamEvent[] | ||
private bufferedLogEntries: LogEntryEvent[] | ||
|
||
constructor(log: LogEntry, sessionId: string) { | ||
this.sessionId = sessionId | ||
this.log = log | ||
this.log.root.events.onAny((_name: string, payload: LogEntryEvent) => { | ||
this.streamLogEntry(payload) | ||
}) | ||
this.bufferedEvents = [] | ||
this.bufferedLogEntries = [] | ||
} | ||
|
||
// TODO: Replace projectName with projectId once we've figured out the flow for that. | ||
connect(eventBus: EventBus, clientAuthToken: string, platformUrl: string, projectName: string) { | ||
this.clientAuthToken = clientAuthToken | ||
this.platformUrl = platformUrl | ||
this.projectName = projectName | ||
|
||
if (!this.intervalId) { | ||
this.startInterval() | ||
} | ||
|
||
if (this.eventBus) { | ||
// We unsubscribe from the old event bus' events. | ||
this.unsubscribeFromGardenEvents(this.eventBus) | ||
} | ||
|
||
this.eventBus = eventBus | ||
this.subscribeToGardenEvents(this.eventBus) | ||
} | ||
|
||
subscribeToGardenEvents(eventBus: EventBus) { | ||
// We maintain this map to facilitate unsubscribing from events when the Garden instance is closed. | ||
const gardenEventListeners = {} | ||
for (const gardenEventName of eventNames) { | ||
const listener = (payload: LogEntryEvent) => this.streamEvent(gardenEventName, payload) | ||
gardenEventListeners[gardenEventName] = listener | ||
eventBus.on(gardenEventName, listener) | ||
} | ||
this.gardenEventListeners = gardenEventListeners | ||
} | ||
|
||
unsubscribeFromGardenEvents(eventBus: EventBus) { | ||
for (const [gardenEventName, listener] of Object.entries(this.gardenEventListeners)) { | ||
eventBus.removeListener(gardenEventName, listener) | ||
} | ||
} | ||
|
||
startInterval() { | ||
this.intervalId = setInterval(() => { | ||
this.flushBuffered({ flushAll: false }) | ||
}, FLUSH_INTERVAL_MSEC) | ||
|
||
registerCleanupFunction("flushAllBufferedEventsAndLogEntries", () => { | ||
this.close() | ||
}) | ||
} | ||
|
||
close() { | ||
if (this.intervalId) { | ||
clearInterval(this.intervalId) | ||
this.intervalId = null | ||
} | ||
this.flushBuffered({ flushAll: true }) | ||
} | ||
|
||
streamEvent<T extends EventName>(name: T, payload: Events[T]) { | ||
this.bufferedEvents.push({ | ||
name, | ||
payload, | ||
timestamp: new Date(), | ||
}) | ||
} | ||
|
||
streamLogEntry(logEntry: LogEntryEvent) { | ||
this.bufferedLogEntries.push(logEntry) | ||
} | ||
|
||
flushEvents(events: StreamEvent[]) { | ||
const data = { | ||
events, | ||
sessionId: this.sessionId, | ||
projectName: this.projectName, | ||
} | ||
const headers = makeAuthHeader(this.clientAuthToken) | ||
got.post(`${this.platformUrl}/events`, { json: data, headers }).catch((err) => { | ||
this.log.error(err) | ||
}) | ||
} | ||
|
||
flushLogEntries(logEntries: LogEntryEvent[]) { | ||
const data = { | ||
logEntries, | ||
sessionId: this.sessionId, | ||
projectName: this.projectName, | ||
} | ||
const headers = makeAuthHeader(this.clientAuthToken) | ||
got.post(`${this.platformUrl}/log-entries`, { json: data, headers }).catch((err) => { | ||
this.log.error(err) | ||
}) | ||
} | ||
|
||
flushBuffered({ flushAll = false }) { | ||
const eventsToFlush = this.bufferedEvents.splice(0, flushAll ? this.bufferedEvents.length : MAX_BATCH_SIZE) | ||
|
||
if (eventsToFlush.length > 0) { | ||
this.flushEvents(eventsToFlush) | ||
} | ||
|
||
const logEntryFlushCount = flushAll ? this.bufferedLogEntries.length : MAX_BATCH_SIZE - eventsToFlush.length | ||
const logEntriesToFlush = this.bufferedLogEntries.splice(0, logEntryFlushCount) | ||
|
||
if (logEntriesToFlush.length > 0) { | ||
this.flushLogEntries(logEntriesToFlush) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.