Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve health check to return more granular details #85

Merged
merged 12 commits into from
Oct 4, 2023
7 changes: 7 additions & 0 deletions pkg/plugin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ func loadSettings(appSettings backend.AppInstanceSettings) Settings {
}
_ = json.Unmarshal(appSettings.JSONData, &settings)

// We need to handle the case where the user has customized the URL,
// then reverted that customization so that the JSON data includes
// an empty string.
if settings.OpenAI.URL == "" {
settings.OpenAI.URL = "https://api.openai.com"
}

switch settings.OpenAI.Provider {
case openAIProviderOpenAI:
case openAIProviderAzure:
Expand Down
47 changes: 27 additions & 20 deletions src/components/AppConfig/AppConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { lastValueFrom } from 'rxjs';

import { css } from '@emotion/css';
import { AppPluginMeta, GrafanaTheme2, KeyValue, PluginConfigPageProps, PluginMeta } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Button, useStyles2 } from '@grafana/ui';
import { FetchResponse, getBackendSrv, HealthCheckResult } from '@grafana/runtime';
import { Button, Spinner, useStyles2 } from '@grafana/ui';

import { testIds } from '../testIds';
import { OpenAIConfig, OpenAISettings } from './OpenAI';
import { VectorConfig, VectorSettings } from './Vector';
import { ShowHealthCheckResult } from './HealthCheck';

export interface AppPluginSettings {
openAI?: OpenAISettings;
Expand Down Expand Up @@ -39,7 +40,10 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
// Whether each secret is already configured in the plugin backend.
const [configuredSecrets, setConfiguredSecrets] = useState<SecretsSet>(initialSecrets(secureJsonFields ?? {}));
// Whether any settings have been updated.
const [updated, setUpdated] = useState(false);
const [updated, setUpdated] = useState(true);

const [isUpdating, setIsUpdating] = useState(false);
const [healthCheck, setHealthCheck] = useState<HealthCheckResult | undefined>(undefined);

return (
<div data-testid={testIds.appConfig.container}>
Expand Down Expand Up @@ -76,7 +80,9 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
<Button
type="submit"
data-testid={testIds.appConfig.submit}
onClick={() => {
onClick={async () => {
setIsUpdating(true);
setHealthCheck(undefined);
let key: keyof SecretsSet;
const secureJsonData: Secrets = {};
for (key in configuredSecrets) {
Expand All @@ -86,18 +92,24 @@ export const AppConfig = ({ plugin }: AppConfigProps) => {
secureJsonData[key] = newSecrets[key];
}
}
updatePluginAndReload(plugin.meta.id, {
await updatePlugin(plugin.meta.id, {
enabled,
pinned,
jsonData: settings,
secureJsonData,
})
});
const result = await checkPluginHealth(plugin.meta.id);
setIsUpdating(false);
setHealthCheck(result.data);
}}
disabled={!updated}
>
Save settings
Save &amp; test
</Button>
{isUpdating && <Spinner />}
sd2k marked this conversation as resolved.
Show resolved Hide resolved
</div>

{healthCheck && <ShowHealthCheckResult {...healthCheck} />}
</div>
);
};
Expand All @@ -111,19 +123,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({
`,
});

const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta<AppPluginSettings>>) => {
try {
await updatePlugin(pluginId, data);

// Reloading the page as the changes made here wouldn't be propagated to the actual plugin otherwise.
// This is not ideal, however unfortunately currently there is no supported way for updating the plugin state.
window.location.reload();
} catch (e) {
console.error('Error while updating the plugin', e);
}
};

export const updatePlugin = async (pluginId: string, data: Partial<PluginMeta>) => {
export const updatePlugin = (pluginId: string, data: Partial<PluginMeta>) => {
const response = getBackendSrv().fetch({
url: `/api/plugins/${pluginId}/settings`,
method: 'POST',
Expand All @@ -132,3 +132,10 @@ export const updatePlugin = async (pluginId: string, data: Partial<PluginMeta>)

return lastValueFrom(response);
};

const checkPluginHealth = (pluginId: string): Promise<FetchResponse<HealthCheckResult>> => {
const response = getBackendSrv().fetch({
url: `/api/plugins/${pluginId}/health`,
});
return lastValueFrom(response) as Promise<FetchResponse<HealthCheckResult>>
}
119 changes: 119 additions & 0 deletions src/components/AppConfig/HealthCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react';

import { HealthCheckResult } from "@grafana/runtime";
import { Alert, AlertVariant } from "@grafana/ui";

interface HealthCheckDetails {
openAI: OpenAIHealthDetails | boolean;
vector: VectorHealthDetails | boolean;
version: string;
}

interface OpenAIHealthDetails {
configured: boolean;
ok: boolean;
error?: string;
models: Record<string, OpenAIModelHealthDetails>;
sd2k marked this conversation as resolved.
Show resolved Hide resolved
}

interface OpenAIModelHealthDetails {
ok: boolean;
error?: string;
}

interface VectorHealthDetails {
enabled: boolean;
ok: boolean;
error?: string;
}

const isHealthCheckDetails = (obj: unknown): obj is HealthCheckDetails => {
return typeof obj === 'object' && obj !== null && 'openAI' in obj && 'vector' in obj && 'version' in obj;
}

const alertVariants = new Set<AlertVariant>(['success', 'info', 'warning', 'error']);
const isAlertVariant = (str: string): str is AlertVariant => alertVariants.has(str as AlertVariant);
const getAlertVariant = (status: string): AlertVariant => {
if (status.toLowerCase() === 'ok') {
return 'success';
}
return isAlertVariant(status) ? status : 'info';
};
const getAlertSeverity = (status: string, details: HealthCheckDetails): AlertVariant => {
const severity = getAlertVariant(status);
if (severity !== 'success') {
return severity;
}
if (!isHealthCheckDetails(details)) {
return 'success';
}
if (typeof details.openAI === 'object' && typeof details.vector === 'object') {
const vectorOk = !details.vector.enabled || details.vector.ok;
return details.openAI.ok && vectorOk ? 'success' : 'warning';
}
return severity;
}

export function ShowHealthCheckResult(props: HealthCheckResult) {
let severity = getAlertVariant(props.status ?? 'error');
if (!isHealthCheckDetails(props.details)) {
return (
<div className="gf-form-group p-t-2">
<Alert severity={severity} title={props.message}>
</Alert>
</div>
);
}

severity = getAlertSeverity(props.status ?? 'error', props.details);
const message = severity === 'success' ? 'Health check succeeded!' : props.message;

return (
<div className="gf-form-group p-t-2">
<Alert severity={severity} title={message}>
<ShowOpenAIHealth openAI={props.details.openAI} />
<ShowVectorHealth vector={props.details.vector} />
sd2k marked this conversation as resolved.
Show resolved Hide resolved
</Alert>
sd2k marked this conversation as resolved.
Show resolved Hide resolved
sd2k marked this conversation as resolved.
Show resolved Hide resolved
</div>
);
}

function ShowOpenAIHealth({ openAI }: { openAI: OpenAIHealthDetails | boolean }) {
if (typeof openAI === 'boolean') {
return <div>OpenAI: {openAI ? 'Enabled' : 'Disabled'}</div>;
}
return (
<div>
<h5>OpenAI</h5>
<div>{openAI.ok ? 'OK' : `Error: ${openAI.error}`}</div>
<b>Models</b>
<table>
<thead>
</thead>
<tbody>
{Object.entries(openAI.models).map(([model, details], i) => (
<tr key={i}>
<td>{model}: </td>
<td>{details.ok ? 'OK' : `Error: ${details.error}`}</td>
</tr>
))}
</tbody>
</table>
</div>
)
sd2k marked this conversation as resolved.
Show resolved Hide resolved
}

function ShowVectorHealth({ vector }: { vector: VectorHealthDetails | boolean }) {
if (typeof vector === 'boolean') {
return <div>Vector: {vector ? 'Enabled' : 'Disabled'}</div>;
}
return (
<div>
<h5>Vector service</h5>
<div>{vector.enabled ? 'Enabled' : 'Disabled'}</div>
{vector.enabled && (
<div>{vector.ok ? 'OK' : `Error: ${vector.error}`}</div>
)}
</div>
)
}