Unify frontend monitoring (#80075)

* Unify frontend monitoring

* Add missing mock

* Add missing mock

* Keep source:sandbox

* Create separate logger for plugins/sql package

* chore: rename "logAlertingError" to "logError"

* Use internal Faro logging for debugging instead of redundant browser logging

* Post-merge fix

* Add more docs about debug levels

* Unify logger names

* Update packages/grafana-runtime/src/utils/logging.ts

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>

* Update packages/grafana-runtime/src/utils/logging.ts

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
Piotr Jamróz 2024-02-01 15:08:40 +01:00 committed by GitHub
parent 7c2622a4f1
commit 572c182a81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 113 additions and 59 deletions

View File

@ -1047,6 +1047,11 @@ instrumentations_console_enabled = false
# Should webvitals instrumentation be enabled, only affects Grafana Javascript Agent
instrumentations_webvitals_enabled = false
# level of internal logging for debugging Grafana Javascript Agent.
# possible values are: 0 = OFF, 1 = ERROR, 2 = WARN, 3 = INFO, 4 = VERBOSE
# more details: https://github.com/grafana/faro-web-sdk/blob/v1.3.7/docs/sources/tutorials/quick-start-browser.md#how-to-activate-debugging
internal_logger_level = 0
# Api Key, only applies to Grafana Javascript Agent provider
api_key =

View File

@ -15,7 +15,7 @@ export {
} from './utils/plugin';
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils';
export { featureEnabled } from './utils/licensing';
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
export { logInfo, logDebug, logWarning, logError, createMonitoringLogger } from './utils/logging';
export {
DataSourceWithBackend,
HealthCheckError,

View File

@ -1,4 +1,4 @@
import { faro, LogLevel, LogContext } from '@grafana/faro-web-sdk';
import { faro, LogContext, LogLevel } from '@grafana/faro-web-sdk';
import { config } from '../config';
@ -57,3 +57,55 @@ export function logError(err: Error, contexts?: LogContext) {
});
}
}
/**
* Creates a monitoring logger with four levels of logging methods: `logDebug`, `logInfo`, `logWarning`, and `logError`.
* These methods use `faro.api.pushX` web SDK methods to report these logs or errors to the Faro collector.
*
* @param {string} source - Identifier for the source of the log messages.
* @param {LogContext} [defaultContext] - Context to be included in every log message.
*
* @returns {Object} Logger object with four methods:
* - `logDebug(message: string, contexts?: LogContext)`: Logs a debug message.
* - `logInfo(message: string, contexts?: LogContext)`: Logs an informational message.
* - `logWarning(message: string, contexts?: LogContext)`: Logs a warning message.
* - `logError(error: Error, contexts?: LogContext)`: Logs an error message.
* Each method combines the `defaultContext` (if provided), the `source`, and an optional `LogContext` parameter into a full context that is included with the log message.
*/
export function createMonitoringLogger(source: string, defaultContext?: LogContext) {
const createFullContext = (contexts?: LogContext) => ({
source: source,
...defaultContext,
...contexts,
});
return {
/**
* Logs a debug message with optional additional context.
* @param {string} message - The debug message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logDebug: (message: string, contexts?: LogContext) => logDebug(message, createFullContext(contexts)),
/**
* Logs an informational message with optional additional context.
* @param {string} message - The informational message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logInfo: (message: string, contexts?: LogContext) => logInfo(message, createFullContext(contexts)),
/**
* Logs a warning message with optional additional context.
* @param {string} message - The warning message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logWarning: (message: string, contexts?: LogContext) => logWarning(message, createFullContext(contexts)),
/**
* Logs an error with optional additional context.
* @param {Error} error - The error object to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logError: (error: Error, contexts?: LogContext) => logError(error, createFullContext(contexts)),
};
}

View File

@ -16,6 +16,9 @@ jest.mock('@grafana/runtime', () => {
},
},
logDebug: jest.fn(),
createMonitoringLogger: jest.fn().mockReturnValue({
logDebug: jest.fn(),
}),
};
});

View File

@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { logDebug, config } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { SQLOptions } from '../../types';
import { sqlPluginLogger } from '../../utils/logging';
/**
* 1. Moves the database field from the options object to jsonData.database and empties the database field.
@ -20,7 +21,7 @@ export function useMigrateDatabaseFields<T extends SQLOptions, S = {}>({
// Migrate the database field from the column into the jsonData object
if (options.database) {
logDebug(`Migrating from options.database with value ${options.database} for ${options.name}`);
sqlPluginLogger.logDebug(`Migrating from options.database with value ${options.database} for ${options.name}`);
newOptions.database = '';
newOptions.jsonData = { ...jsonData, database: options.database };
optionsUpdated = true;
@ -35,7 +36,7 @@ export function useMigrateDatabaseFields<T extends SQLOptions, S = {}>({
) {
const { maxOpenConns, maxIdleConns } = config.sqlConnectionLimits;
logDebug(
sqlPluginLogger.logDebug(
`Setting default max open connections to ${maxOpenConns} and setting max idle connection to ${maxIdleConns}`
);

View File

@ -0,0 +1,3 @@
import { createMonitoringLogger } from '@grafana/runtime';
export const sqlPluginLogger = createMonitoringLogger('features.plugins.sql');

View File

@ -8,6 +8,7 @@ type GrafanaJavascriptAgent struct {
ErrorInstrumentalizationEnabled bool `json:"errorInstrumentalizationEnabled"`
ConsoleInstrumentalizationEnabled bool `json:"consoleInstrumentalizationEnabled"`
WebVitalsInstrumentalizationEnabled bool `json:"webVitalsInstrumentalizationEnabled"`
InternalLoggerLevel int `json:"internalLoggerLevel"`
ApiKey string `json:"apiKey"`
}
@ -21,6 +22,7 @@ func (cfg *Cfg) readGrafanaJavascriptAgentConfig() {
ErrorInstrumentalizationEnabled: raw.Key("instrumentations_errors_enabled").MustBool(true),
ConsoleInstrumentalizationEnabled: raw.Key("instrumentations_console_enabled").MustBool(true),
WebVitalsInstrumentalizationEnabled: raw.Key("instrumentations_webvitals_enabled").MustBool(true),
InternalLoggerLevel: raw.Key("internal_logger_level").MustInt(0),
ApiKey: raw.Key("api_key").String(),
}
}

View File

@ -1,5 +1,5 @@
import { BuildInfo } from '@grafana/data';
import { BaseTransport } from '@grafana/faro-core';
import { BaseTransport, defaultInternalLoggerLevel } from '@grafana/faro-core';
import {
initializeFaro,
BrowserConfig,
@ -74,6 +74,7 @@ export class GrafanaJavascriptAgentBackend
batching: {
sendTimeout: 1000,
},
internalLoggerLevel: options.internalLoggerLevel || defaultInternalLoggerLevel,
};
this.faroInstance = initializeFaro(grafanaJavaScriptAgentOptions);

View File

@ -1,6 +1,5 @@
import { dateTime } from '@grafana/data';
import { faro, LogLevel as GrafanaLogLevel } from '@grafana/faro-web-sdk';
import { getBackendSrv, logError } from '@grafana/runtime';
import { createMonitoringLogger, getBackendSrv } from '@grafana/runtime';
import { config, reportInteraction } from '@grafana/runtime/src';
import { contextSrv } from 'app/core/core';
@ -22,18 +21,14 @@ export const LogMessages = {
unknownMessageFromError: 'unknown messageFromError',
};
// logInfo from '@grafana/runtime' should be used, but it doesn't handle Grafana JS Agent correctly
export function logInfo(message: string, context: Record<string, string | number> = {}) {
if (config.grafanaJavascriptAgent.enabled) {
faro.api.pushLog([message], {
level: GrafanaLogLevel.INFO,
context: { ...context, module: 'Alerting' },
});
}
const alertingLogger = createMonitoringLogger('features.alerting', { module: 'Alerting' });
export function logInfo(message: string, context?: Record<string, string>) {
alertingLogger.logInfo(message, context);
}
export function logAlertingError(error: Error, context: Record<string, string | number> = {}) {
logError(error, { ...context, module: 'Alerting' });
export function logError(error: Error, context?: Record<string, string>) {
alertingLogger.logError(error, context);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -10,7 +10,7 @@ import { useCleanup } from 'app/core/hooks/useCleanup';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { getMessageFromError } from '../../../../../../core/utils/errors';
import { logAlertingError } from '../../../Analytics';
import { logError } from '../../../Analytics';
import { isOnCallFetchError } from '../../../api/onCallApi';
import { useControlledFieldArray } from '../../../hooks/useControlledFieldArray';
import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '../../../types/receiver-form';
@ -103,7 +103,7 @@ export function ReceiverForm<R extends ChannelValues>({
const error = new Error('Failed to save the contact point');
error.cause = e;
logAlertingError(error);
logError(error);
}
throw e;
}

View File

@ -1,12 +1,10 @@
import * as comlink from 'comlink';
import { useCallback, useEffect } from 'react';
import { logError } from '@grafana/runtime';
import { AlertmanagerGroup, RouteWithID } from '../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../types/unified-alerting-dto';
import { logInfo } from './Analytics';
import { logError, logInfo } from './Analytics';
import { createWorker } from './createRouteGroupsMatcherWorker';
import type { RouteGroupsMatcher } from './routeGroupsMatcher';

View File

@ -2,7 +2,7 @@ import { useAsyncFn } from 'react-use';
import { lastValueFrom } from 'rxjs';
import { DataSourceInstanceSettings } from '@grafana/data';
import { getDataSourceSrv, FetchResponse, logWarning } from '@grafana/runtime';
import { getDataSourceSrv, FetchResponse } from '@grafana/runtime';
import { useGrafana } from 'app/core/context/GrafanaContext';
import {
@ -15,6 +15,7 @@ import {
UpdateCorrelationParams,
UpdateCorrelationResponse,
} from './types';
import { correlationsLogger } from './utils';
export interface CorrelationsResponse {
correlations: Correlation[];
@ -47,7 +48,7 @@ const toEnrichedCorrelationData = ({
// This logging is to check if there are any customers who did not migrate existing correlations.
// See Deprecation Notice in https://github.com/grafana/grafana/pull/72258 for more details
if (correlation?.orgId === undefined || correlation?.orgId === null || correlation?.orgId === 0) {
logWarning('Invalid correlation config: Missing org id.', { module: 'Explore' });
correlationsLogger.logWarning('Invalid correlation config: Missing org id.');
}
if (
@ -62,8 +63,7 @@ const toEnrichedCorrelationData = ({
target: targetDatasource,
};
} else {
logWarning(`Invalid correlation config: Missing source or target.`, {
module: 'Explore',
correlationsLogger.logWarning(`Invalid correlation config: Missing source or target.`, {
source: JSON.stringify(sourceDatasource),
target: JSON.stringify(targetDatasource),
});

View File

@ -1,7 +1,7 @@
import { lastValueFrom } from 'rxjs';
import { DataFrame, DataLinkConfigOrigin } from '@grafana/data';
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { createMonitoringLogger, getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { ExploreItemState } from 'app/types';
import { formatValueName } from '../explore/PrometheusListView/ItemLabels';
@ -108,3 +108,5 @@ export const generateDefaultLabel = async (sourcePane: ExploreItemState, targetP
: '';
});
};
export const correlationsLogger = createMonitoringLogger('features.correlations');

View File

@ -3,7 +3,7 @@ import { useAsync } from 'react-use';
import { Subscription } from 'rxjs';
import { llms } from '@grafana/experimental';
import { logError } from '@grafana/runtime';
import { createMonitoringLogger } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
import { isLLMPluginEnabled, DEFAULT_OAI_MODEL } from './utils';
@ -12,6 +12,8 @@ import { isLLMPluginEnabled, DEFAULT_OAI_MODEL } from './utils';
// Ideally we will want to move the hook itself to a different scope later.
type Message = llms.openai.Message;
const genAILogger = createMonitoringLogger('features.dashboards.genai');
export enum StreamStatus {
IDLE = 'idle',
GENERATING = 'generating',
@ -62,7 +64,7 @@ export function useOpenAIStream(
`Please try again or if the problem persists, contact your organization admin.`
);
console.error(e);
logError(e, { messages: JSON.stringify(messages), model, temperature: String(temperature) });
genAILogger.logError(e, { messages: JSON.stringify(messages), model, temperature: String(temperature) });
},
[messages, model, temperature, notifyError]
);

View File

@ -13,7 +13,7 @@ import {
PluginType,
PluginContextProvider,
} from '@grafana/data';
import { config, locationSearchToObject, logError } from '@grafana/runtime';
import { config, locationSearchToObject } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
@ -25,7 +25,7 @@ import { getMessageFromError } from 'app/core/utils/errors';
import { getPluginSettings } from '../pluginSettings';
import { importAppPlugin } from '../plugin_loader';
import { buildPluginSectionNav } from '../utils';
import { buildPluginSectionNav, pluginsLogger } from '../utils';
import { buildPluginPageContext, PluginPageContext } from './PluginPageContext';
@ -193,7 +193,7 @@ async function loadAppPlugin(pluginId: string, dispatch: React.Dispatch<AnyActio
})
);
const error = err instanceof Error ? err : new Error(getMessageFromError(err));
logError(error);
pluginsLogger.logError(error);
console.error(error);
}
}

View File

@ -3,12 +3,7 @@ import React from 'react';
import { PluginSignatureType, PluginType } from '@grafana/data';
import { LogContext } from '@grafana/faro-web-sdk';
import {
logWarning as logWarningRuntime,
logError as logErrorRuntime,
logInfo as logInfoRuntime,
config,
} from '@grafana/runtime';
import { config, createMonitoringLogger } from '@grafana/runtime';
import { getPluginSettings } from '../pluginSettings';
@ -24,35 +19,22 @@ export function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}. This should never happen.`);
}
const sandboxLogger = createMonitoringLogger('sandbox', { monitorOnly: String(monitorOnly) });
export function isReactClassComponent(obj: unknown): obj is React.Component {
return obj instanceof React.Component;
}
export function logWarning(message: string, context?: LogContext) {
context = {
...context,
source: 'sandbox',
monitorOnly: String(monitorOnly),
};
logWarningRuntime(message, context);
sandboxLogger.logWarning(message, context);
}
export function logError(error: Error, context?: LogContext) {
context = {
...context,
source: 'sandbox',
monitorOnly: String(monitorOnly),
};
logErrorRuntime(error, context);
sandboxLogger.logError(error, context);
}
export function logInfo(message: string, context?: LogContext) {
context = {
...context,
source: 'sandbox',
monitorOnly: String(monitorOnly),
};
logInfoRuntime(message, context);
sandboxLogger.logInfo(message, context);
}
export async function isFrontendSandboxSupported({

View File

@ -1,4 +1,5 @@
import { GrafanaPlugin, NavModel, NavModelItem, PanelPluginMeta, PluginType } from '@grafana/data';
import { createMonitoringLogger } from '@grafana/runtime';
import { importPanelPluginFromMeta } from './importPanelPlugin';
import { getPluginSettings } from './pluginSettings';
@ -83,3 +84,5 @@ export function buildPluginSectionNav(
return { main: copiedPluginNavSection, node: activePage ?? copiedPluginNavSection };
}
export const pluginsLogger = createMonitoringLogger('features.plugins');

View File

@ -18,13 +18,15 @@ import {
PanelData,
TimeRange,
} from '@grafana/data';
import { config, toDataQueryError, logError } from '@grafana/runtime';
import { config, toDataQueryError } from '@grafana/runtime';
import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend';
import { backendSrv } from 'app/core/services/backend_srv';
import { queryIsEmpty } from 'app/core/utils/query';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { ExpressionQuery } from 'app/features/expressions/types';
import { queryLogger } from '../utils';
import { cancelNetworkRequestsOnUnsubscribe } from './processing/canceler';
import { emitDataRequestEvent } from './queryAnalytics';
@ -157,7 +159,7 @@ export function runRequest(
// handle errors
catchError((err) => {
console.error('runRequest.catchError', err);
logError(err);
queryLogger.logError(err);
return of({
...state.panelData,
state: LoadingState.Error,

View File

@ -0,0 +1,3 @@
import { createMonitoringLogger } from '@grafana/runtime';
export const queryLogger = createMonitoringLogger('features.query');