Add remoteLog function for gathering gui logs (#6582)

Co-authored-by: Michał W. Urbańczyk <mwu-tow@gazeta.pl>
Co-authored-by: Nikita Pekin <nikita@frecency.com>
This commit is contained in:
Paweł Buchowski 2023-07-12 12:22:07 +02:00 committed by GitHub
parent af03938305
commit 1d77f7cd8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 154 additions and 43 deletions

View File

@ -18,17 +18,15 @@ mod js {
#[wasm_bindgen(inline_js = "
export function remote_log(msg, value) {
try {
window.ensoglApp.remoteLog(msg,value)
} catch (error) {
console.error(\"Error while logging message. \" + error );
}
window.ensoglApp.remoteLog(msg, value).catch((error) => {
console.error(`Error while logging message. ${error}`)
})
}
export function remote_log_value(msg, field_name, value) {
const data = {}
data[field_name] = value
remote_log(msg,data)
remote_log(msg, data)
}
")]
extern "C" {

View File

@ -3,11 +3,6 @@
"authentication": {
"value": true,
"description": "Determines whether user authentication is enabled. This option is always true when executed in the cloud."
},
"dataCollection": {
"value": true,
"description": "Determines whether anonymous usage data is to be collected.",
"primary": false
}
},
"groups": {

View File

@ -10,6 +10,7 @@ import * as dashboard from 'enso-authentication'
import * as detect from 'enso-common/src/detect'
import * as app from '../../../../../target/ensogl-pack/linked-dist'
import * as remoteLog from './remoteLog'
import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' }
const logger = app.log.logger
@ -159,7 +160,7 @@ class Main implements AppRunner {
/** Run an app instance with the specified configuration.
* This includes the scene to run and the WebSocket endpoints to the backend. */
async runApp(inputConfig?: StringConfig | null) {
async runApp(inputConfig?: StringConfig | null, accessToken?: string) {
this.stopApp()
/** FIXME: https://github.com/enso-org/enso/issues/6475
@ -174,7 +175,7 @@ class Main implements AppRunner {
inputConfig
)
this.app = new app.App({
const newApp = new app.App({
config,
configOptions: contentConfig.OPTIONS,
packageInfo: {
@ -183,12 +184,27 @@ class Main implements AppRunner {
},
})
// We override the remote logger stub with the "real" one. Eventually the runner should not be aware of the
// remote logger at all, and it should be integrated with our logging infrastructure.
const remoteLogger = accessToken != null ? new remoteLog.RemoteLogger(accessToken) : null
newApp.remoteLog = async (message: string, metadata: unknown) => {
if (newApp.config.options.dataCollection.value && remoteLogger) {
await remoteLogger.remoteLog(message, metadata)
} else {
const logMessage = [
'Not sending log to remote server. Data collection is disabled.',
`Message: "${message}"`,
`Metadata: ${JSON.stringify(metadata)}`,
].join(' ')
logger.log(logMessage)
}
}
this.app = newApp
if (!this.app.initialized) {
console.error('Failed to initialize the application.')
} else {
if (contentConfig.OPTIONS.options.dataCollection.value) {
// TODO: Add remote-logging here.
}
if (!(await checkMinSupportedVersion(contentConfig.OPTIONS))) {
displayDeprecatedVersionDialog()
} else {
@ -270,7 +286,7 @@ class Main implements AppRunner {
isAuthenticationDisabled: !config.shouldUseAuthentication,
shouldShowDashboard: config.shouldUseNewDashboard,
initialProjectName: config.initialProjectName,
onAuthenticated: () => {
onAuthenticated: (accessToken?: string) => {
if (config.isInAuthenticationFlow) {
const initialUrl = localStorage.getItem(INITIAL_URL_KEY)
if (initialUrl != null) {
@ -287,7 +303,7 @@ class Main implements AppRunner {
ideElement.style.display = ''
}
if (this.app == null) {
void this.runApp(config.inputConfig)
void this.runApp(config.inputConfig, accessToken)
}
}
},

View File

@ -0,0 +1,75 @@
/** @file Defines the {@link RemoteLogger} class and {@link remoteLog} function for sending logs to a remote server.
* {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */
import * as app from '../../../../../target/ensogl-pack/linked-dist'
import * as authConfig from '../../dashboard/src/authentication/src/config'
const logger = app.log.logger
// =================
// === Constants ===
// =================
/** URL address where remote logs should be sent. */
const REMOTE_LOG_URL = new URL(`${authConfig.ACTIVE_CONFIG.apiUrl}/logs`)
// ====================
// === RemoteLogger ===
// ====================
// === Class ===
/** Helper class facilitating sending logs to a remote. */
export class RemoteLogger {
/** Initialize a new instance.
* @param accessToken - JWT token used to authenticate within the cloud. */
constructor(public accessToken: string) {
this.accessToken = accessToken
}
/** Sends a log message to a remote.
* @param message - The log message to send.
* @param metadata - Additional metadata to send along with the log.
* @returns Promise which resolves when the log message has been sent. */
async remoteLog(message: string, metadata: unknown): Promise<void> {
await remoteLog(this.accessToken, message, metadata)
}
}
// === Underlying logic ===
/** Sends a log message to a remote server using the provided access token.
*
* @param accessToken - The access token for authentication.
* @param message - The message to be logged on the server.
* @param metadata - Additional metadata to include in the log.
* @throws Will throw an error if the response from the server is not okay (response status is not 200).
* @returns Returns a promise that resolves when the log message is successfully sent. */
export async function remoteLog(
accessToken: string,
message: string,
metadata: unknown
): Promise<void> {
try {
const headers: HeadersInit = new Headers()
headers.set('Content-Type', 'application/json')
headers.set('Authorization', `Bearer ${accessToken}`)
const response = await fetch(REMOTE_LOG_URL, {
method: 'POST',
headers,
body: JSON.stringify({ message, metadata }),
})
if (!response.ok) {
const errorMessage = `Error while sending log to a remote: Status ${response.status}.`
try {
const text = await response.text()
throw new Error(`${errorMessage} Response: ${text}.`)
} catch (error) {
throw new Error(`${errorMessage} Failed to read response: ${String(error)}.`)
}
}
} catch (error) {
logger.error(error)
throw error
}
}

View File

@ -166,7 +166,7 @@ export interface AuthProviderProps {
supportsLocalBackend: boolean
authService: authServiceModule.AuthService
/** Callback to execute once the user has authenticated successfully. */
onAuthenticated: () => void
onAuthenticated: (accessToken?: string) => void
children: React.ReactNode
}
@ -290,7 +290,7 @@ export function AuthProvider(props: AuthProviderProps) {
/** Execute the callback that should inform the Electron app that the user has logged in.
* This is done to transition the app from the authentication/dashboard view to the IDE. */
onAuthenticated()
onAuthenticated(accessToken)
}
setUserSession(newUserSession)

View File

@ -62,6 +62,17 @@ const BASE_AMPLIFY_CONFIG = {
/** Collection of configuration details for Amplify user pools, sorted by deployment environment. */
const AMPLIFY_CONFIGS = {
/** Configuration for @indiv0's Cognito user pool. */
npekin: {
userPoolId: newtype.asNewtype<auth.UserPoolId>('eu-west-1_AXX1gMvpx'),
userPoolWebClientId: newtype.asNewtype<auth.UserPoolWebClientId>(
'1rpnb2n1ijn6o5529a7ob017o'
),
domain: newtype.asNewtype<auth.OAuthDomain>(
'npekin-enso-domain.auth.eu-west-1.amazoncognito.com'
),
...BASE_AMPLIFY_CONFIG,
} satisfies Partial<auth.AmplifyConfig>,
/** Configuration for @pbuchu's Cognito user pool. */
pbuchu: {
userPoolId: newtype.asNewtype<auth.UserPoolId>('eu-west-1_jSF1RbgPK'),

View File

@ -26,11 +26,16 @@ const CLOUD_REDIRECTS = {
/** All possible API URLs, sorted by environment. */
const API_URLS = {
pbuchu: newtype.asNewtype<ApiUrl>('https://xw0g8j3tsb.execute-api.eu-west-1.amazonaws.com'),
npekin: newtype.asNewtype<ApiUrl>('https://s02ejyepk1.execute-api.eu-west-1.amazonaws.com'),
production: newtype.asNewtype<ApiUrl>('https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com'),
}
/** All possible configuration options, sorted by environment. */
const CONFIGS = {
npekin: {
cloudRedirect: CLOUD_REDIRECTS.development,
apiUrl: API_URLS.npekin,
} satisfies Config,
pbuchu: {
cloudRedirect: CLOUD_REDIRECTS.development,
apiUrl: API_URLS.pbuchu,
@ -61,7 +66,7 @@ export interface Config {
/** Possible values for the environment/user we're running for and whose infrastructure we're
* testing against. */
export type Environment = 'pbuchu' | 'production'
export type Environment = 'npekin' | 'pbuchu' | 'production'
// ===========
// === API ===

View File

@ -1,6 +1,7 @@
/** @file Container that launches the IDE. */
import * as React from 'react'
import * as auth from '../../authentication/providers/auth'
import * as backendModule from '../backend'
import * as backendProvider from '../../providers/backend'
@ -26,10 +27,11 @@ export interface IdeProps {
appRunner: AppRunner
}
/** The ontainer that launches the IDE. */
/** The container that launches the IDE. */
function Ide(props: IdeProps) {
const { project, appRunner } = props
const { backend } = backendProvider.useBackend()
const { accessToken } = auth.useNonPartialUserSession()
React.useEffect(() => {
void (async () => {
@ -78,20 +80,25 @@ function Ide(props: IdeProps) {
: {
projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint,
}
await appRunner.runApp({
loader: {
assetsUrl: `${assetsRoot}dynamic-assets`,
wasmUrl: `${assetsRoot}pkg-opt.wasm`,
jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`,
await appRunner.runApp(
{
loader: {
assetsUrl: `${assetsRoot}dynamic-assets`,
wasmUrl: `${assetsRoot}pkg-opt.wasm`,
jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`,
},
engine: {
...engineConfig,
preferredVersion: engineVersion,
},
startup: {
project: project.packageName,
},
},
engine: {
...engineConfig,
preferredVersion: engineVersion,
},
startup: {
project: project.packageName,
},
})
// Here we actually need explicit undefined.
// eslint-disable-next-line no-restricted-syntax
accessToken ?? undefined
)
}
if (backend.type === backendModule.BackendType.local) {
await runNewProject()

View File

@ -13,5 +13,5 @@ interface StringConfig {
* open a new IDE instance. */
interface AppRunner {
stopApp: () => void
runApp: (config?: StringConfig) => Promise<void>
runApp: (config?: StringConfig, accessToken?: string) => Promise<void>
}

View File

@ -3,6 +3,10 @@
"debug": {
"value": false,
"description": "Controls the debug mode for the application. In this mode, all logs are printed to the console, and EnsoGL extensions are loaded. Otherwise, logs are hidden unless explicitly shown with 'showLogs'."
},
"dataCollection": {
"value": true,
"description": "Determines whether anonymous usage data is to be collected."
}
},
"groups": {

View File

@ -238,12 +238,11 @@ export class App {
}
/** Log the message on the remote server. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
remoteLog(message: string, data: any) {
// FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/359
// Implement remote logging. This should be done after cloud integration.
// Function interface is left intentionally for readability.
// Remove typescript error suppression after resolving fixme.
// This method is assumed to be overriden by the App's owner. Eventually it should be removed from the runner
// altogether, as it is not its responsibility.
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
async remoteLog(message: string, data: any) {
console.warn('Remote logging is not set up.')
}
/** Initialize the browser. Set the background color, print user-facing warnings, etc. */
@ -281,8 +280,9 @@ export class App {
}
}
}
/** Sets application stop to true and calls drop method which removes all rust memory references
* and calls all destructors. */
* and calls all destructors. */
stop() {
this.stopped = true
this.wasm?.drop()