## Description 1. Added frontend and backend custom OTLP telemetry to track execute flow 2. Updated end vars in client side code to match with server sdk intialisation code. #### PR fixes following issue(s) Fixes #28800 and #28805 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) #### How Has This Been Tested? - [x] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
parent
dea2fd736c
commit
2f6f824efc
|
|
@ -53,6 +53,8 @@ server {
|
|||
sub_filter __APPSMITH_NEW_RELIC_BROWSER_AGENT_LICENSE_KEY__ '${APPSMITH_NEW_RELIC_BROWSER_AGENT_LICENSE_KEY}';
|
||||
sub_filter __APPSMITH_NEW_RELIC_ACCOUNT_ENABLE__ '${APPSMITH_NEW_RELIC_ACCOUNT_ENABLE}';
|
||||
sub_filter __APPSMITH_NEW_RELIC_OTLP_LICENSE_KEY__ '${APPSMITH_NEW_RELIC_OTLP_LICENSE_KEY}';
|
||||
sub_filter __APPSMITH_NEW_RELIC_OTEL_SERVICE_NAME__ '${APPSMITH_NEW_RELIC_OTEL_SERVICE_NAME}';
|
||||
sub_filter __APPSMITH_NEW_RELIC_OTEL_EXPORTER_OTLP_ENDPOINT__ '${APPSMITH_NEW_RELIC_OTEL_EXPORTER_OTLP_ENDPOINT}';
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ module.exports = {
|
|||
applicationId: parseConfig("__APPSMITH_NEW_RELIC_APPLICATION_ID__"),
|
||||
browserAgentlicenseKey: parseConfig("__APPSMITH_NEW_RELIC_BROWSER_AGENT_LICENSE_KEY__"),
|
||||
otlpLicenseKey: parseConfig("__APPSMITH_NEW_RELIC_OTLP_LICENSE_KEY__"),
|
||||
otlpServiceName: parseConfig("__APPSMITH_NEW_RELIC_OTEL_SERVICE_NAME__"),
|
||||
otlpEndpoint:parseConfig("__APPSMITH_NEW_RELIC_OTEL_EXPORTER_OTLP_ENDPOINT__")
|
||||
},
|
||||
fusioncharts: {
|
||||
licenseKey: parseConfig("__APPSMITH_FUSIONCHARTS_LICENSE_KEY__"),
|
||||
|
|
|
|||
|
|
@ -276,6 +276,10 @@
|
|||
applicationId: parseConfig("__APPSMITH_NEW_RELIC_APPLICATION_ID__"),
|
||||
browserAgentlicenseKey: parseConfig("__APPSMITH_NEW_RELIC_BROWSER_AGENT_LICENSE_KEY__"),
|
||||
otlpLicenseKey: parseConfig("__APPSMITH_NEW_RELIC_OTLP_LICENSE_KEY__"),
|
||||
//OTLP following the naming convention of Sdk initialisation
|
||||
otlpServiceName: parseConfig("__APPSMITH_NEW_RELIC_OTEL_SERVICE_NAME__"),
|
||||
otlpEndpoint:parseConfig("__APPSMITH_NEW_RELIC_OTEL_EXPORTER_OTLP_ENDPOINT__"),
|
||||
|
||||
},
|
||||
fusioncharts: {
|
||||
licenseKey: parseConfig("__APPSMITH_FUSIONCHARTS_LICENSE_KEY__"),
|
||||
|
|
|
|||
|
|
@ -7,23 +7,22 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|||
import { Resource } from "@opentelemetry/resources";
|
||||
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
|
||||
import { getAppsmithConfigs } from "@appsmith/configs";
|
||||
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
||||
|
||||
const { newRelic } = getAppsmithConfigs();
|
||||
const { applicationId, otlpLicenseKey } = newRelic;
|
||||
|
||||
const NEW_RELIC_OTLP_ENTITY_NAME = "Appsmith Frontend OTLP";
|
||||
const NEW_RELIC_OTLP_ENDPOINT = "https://otlp.nr-data.net:4318";
|
||||
const { applicationId, otlpEndpoint, otlpLicenseKey, otlpServiceName } =
|
||||
newRelic;
|
||||
|
||||
const provider = new WebTracerProvider({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: NEW_RELIC_OTLP_ENTITY_NAME,
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: otlpServiceName,
|
||||
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: applicationId,
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
|
||||
}),
|
||||
});
|
||||
|
||||
const newRelicExporter = new OTLPTraceExporter({
|
||||
url: `${NEW_RELIC_OTLP_ENDPOINT}/v1/traces`,
|
||||
url: `${otlpEndpoint}/v1/traces`,
|
||||
headers: {
|
||||
"api-key": otlpLicenseKey,
|
||||
},
|
||||
|
|
@ -43,9 +42,28 @@ const processor = new BatchSpanProcessor(
|
|||
exportTimeoutMillis: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
const W3C_OTLP_TRACE_HEADER = "traceparent";
|
||||
const CUSTOM_OTLP_TRACE_HEADER = "traceparent-otlp";
|
||||
//We are overriding the default header "traceparent" used for trace context because the browser
|
||||
// agent shares the same header's distributed tracing
|
||||
class CustomW3CTraceContextPropagator extends W3CTraceContextPropagator {
|
||||
inject(context, carrier, setter) {
|
||||
// Call the original inject method to get the default traceparent header
|
||||
super.inject(context, carrier, setter);
|
||||
|
||||
// Modify the carrier to use a different header
|
||||
if (carrier[W3C_OTLP_TRACE_HEADER]) {
|
||||
carrier[CUSTOM_OTLP_TRACE_HEADER] = carrier[W3C_OTLP_TRACE_HEADER];
|
||||
delete carrier[W3C_OTLP_TRACE_HEADER]; // Remove the original traceparent header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider.addSpanProcessor(processor);
|
||||
provider.register({
|
||||
contextManager: new ZoneContextManager(),
|
||||
propagator: new CustomW3CTraceContextPropagator(),
|
||||
});
|
||||
|
||||
registerInstrumentations({
|
||||
89
app/client/src/UITelemetry/generateTraces.ts
Normal file
89
app/client/src/UITelemetry/generateTraces.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { Span, Attributes, HrTime } from "@opentelemetry/api";
|
||||
import { SpanKind } from "@opentelemetry/api";
|
||||
import { context } from "@opentelemetry/api";
|
||||
import { trace } from "@opentelemetry/api";
|
||||
|
||||
const GENERATOR_TRACE = "generator-tracer";
|
||||
export function startRootSpan(spanName: string, spanAttributes?: Attributes) {
|
||||
const tracer = trace.getTracer(GENERATOR_TRACE);
|
||||
if (!spanName) {
|
||||
return;
|
||||
}
|
||||
const attributes = spanAttributes ?? { attributes: spanAttributes };
|
||||
return tracer?.startSpan(spanName, { kind: SpanKind.CLIENT, ...attributes });
|
||||
}
|
||||
export const generateContext = (span: Span) => {
|
||||
return trace.setSpan(context.active(), span);
|
||||
};
|
||||
export function startNestedSpan(
|
||||
spanName: string,
|
||||
parentSpan: Span,
|
||||
spanAttributes?: Attributes,
|
||||
) {
|
||||
if (!spanName || !parentSpan) {
|
||||
// do not generate nested span without parentSpan..we cannot generate context out of it
|
||||
return;
|
||||
}
|
||||
|
||||
const parentContext = generateContext(parentSpan);
|
||||
|
||||
const generatorTrace = trace.getTracer(GENERATOR_TRACE);
|
||||
if (!spanAttributes) {
|
||||
return generatorTrace.startSpan(
|
||||
spanName,
|
||||
{ kind: SpanKind.CLIENT },
|
||||
parentContext,
|
||||
);
|
||||
}
|
||||
return generatorTrace.startSpan(
|
||||
spanName,
|
||||
{ attributes: { kind: SpanKind.CLIENT, ...spanAttributes } },
|
||||
parentContext,
|
||||
);
|
||||
}
|
||||
|
||||
function convertHighResolutionTimeToEpoch(hr: HrTime) {
|
||||
const epochInSeconds = hr[0];
|
||||
const millisecondFragment = Math.round(hr[1] / 1000000);
|
||||
const epochInMilliseconds = epochInSeconds * 1000 + millisecondFragment;
|
||||
return epochInMilliseconds;
|
||||
}
|
||||
|
||||
function addTraceToNewRelicSession(span: any) {
|
||||
if (
|
||||
!span ||
|
||||
!span.startTime ||
|
||||
!span.endTime ||
|
||||
!span.name ||
|
||||
!(window as any)?.newrelic
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
//extract timestamp details from the span
|
||||
//we have to convert it from HR timestamp to a regular epoch
|
||||
const start = convertHighResolutionTimeToEpoch(span.startTime);
|
||||
const end = convertHighResolutionTimeToEpoch(span.endTime);
|
||||
const spanName = span.name;
|
||||
|
||||
//the new relic window object is attached when the browser script
|
||||
(window as any).newrelic.addToTrace({ name: spanName, start, end });
|
||||
}
|
||||
export function endSpan(span?: Span) {
|
||||
span?.end();
|
||||
|
||||
addTraceToNewRelicSession(span);
|
||||
}
|
||||
export function setAttributesToSpan(span: Span, spanAttributes: Attributes) {
|
||||
if (!span) {
|
||||
return;
|
||||
}
|
||||
span.setAttributes(spanAttributes);
|
||||
}
|
||||
|
||||
export function wrapFnWithParentTraceContext(parentSpan: Span, fn: () => any) {
|
||||
const parentContext = trace.setSpan(context.active(), parentSpan);
|
||||
return context.with(parentContext, fn);
|
||||
}
|
||||
|
||||
export type OtlpSpan = Span;
|
||||
|
|
@ -14,6 +14,7 @@ import type { Action } from "entities/Action";
|
|||
import { batchAction } from "actions/batchActions";
|
||||
import type { ExecuteErrorPayload } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import type { ModalInfo } from "reducers/uiReducers/modalActionReducer";
|
||||
import type { OtlpSpan } from "UITelemetry/generateTraces";
|
||||
|
||||
export const createActionRequest = (payload: Partial<Action>) => {
|
||||
return {
|
||||
|
|
@ -345,17 +346,30 @@ export const bindDataOnCanvas = (payload: {
|
|||
};
|
||||
};
|
||||
|
||||
type actionDataPayload = {
|
||||
entityName: string;
|
||||
dataPath: string;
|
||||
data: unknown;
|
||||
dataPathRef?: string;
|
||||
}[];
|
||||
|
||||
export interface updateActionDataPayloadType {
|
||||
actionDataPayload: actionDataPayload;
|
||||
parentSpan?: OtlpSpan;
|
||||
}
|
||||
export const updateActionData = (
|
||||
payload: {
|
||||
entityName: string;
|
||||
dataPath: string;
|
||||
data: unknown;
|
||||
dataPathRef?: string;
|
||||
}[],
|
||||
) => {
|
||||
payload: actionDataPayload,
|
||||
parentSpan?: OtlpSpan,
|
||||
): {
|
||||
type: string;
|
||||
payload: updateActionDataPayloadType;
|
||||
} => {
|
||||
return {
|
||||
type: ReduxActionTypes.UPDATE_ACTION_DATA,
|
||||
payload,
|
||||
payload: {
|
||||
actionDataPayload: payload,
|
||||
parentSpan,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import type { Action, ActionViewMode } from "entities/Action";
|
|||
import type { APIRequest } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import type { WidgetType } from "constants/WidgetConstants";
|
||||
import { omit } from "lodash";
|
||||
import type { OtlpSpan } from "UITelemetry/generateTraces";
|
||||
import { wrapFnWithParentTraceContext } from "UITelemetry/generateTraces";
|
||||
|
||||
export interface CreateActionRequest<T> extends APIRequest {
|
||||
datasourceId: string;
|
||||
|
|
@ -201,12 +203,10 @@ class ActionAPI extends API {
|
|||
static async deleteAction(id: string) {
|
||||
return API.delete(`${ActionAPI.url}/${id}`);
|
||||
}
|
||||
|
||||
static async executeAction(
|
||||
private static async executeApiCall(
|
||||
executeAction: FormData,
|
||||
timeout?: number,
|
||||
): Promise<AxiosPromise<ActionExecutionResponse>> {
|
||||
ActionAPI.abortActionExecutionTokenSource = axios.CancelToken.source();
|
||||
return API.post(ActionAPI.url + "/execute", executeAction, undefined, {
|
||||
timeout: timeout || DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
|
||||
headers: {
|
||||
|
|
@ -218,6 +218,20 @@ class ActionAPI extends API {
|
|||
});
|
||||
}
|
||||
|
||||
static async executeAction(
|
||||
executeAction: FormData,
|
||||
timeout?: number,
|
||||
parentSpan?: OtlpSpan,
|
||||
): Promise<AxiosPromise<ActionExecutionResponse>> {
|
||||
ActionAPI.abortActionExecutionTokenSource = axios.CancelToken.source();
|
||||
if (!parentSpan) {
|
||||
return this.executeApiCall(executeAction, timeout);
|
||||
}
|
||||
return wrapFnWithParentTraceContext(parentSpan, async () => {
|
||||
return await this.executeApiCall(executeAction, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
static async moveAction(moveRequest: MoveActionRequest) {
|
||||
return API.put(ActionAPI.url + "/move", moveRequest, undefined, {
|
||||
timeout: DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export interface INJECTED_CONFIGS {
|
|||
applicationId: string;
|
||||
browserAgentlicenseKey: string;
|
||||
otlpLicenseKey: string;
|
||||
otlpServiceName: string;
|
||||
otlpEndpoint: string;
|
||||
};
|
||||
fusioncharts: {
|
||||
licenseKey: string;
|
||||
|
|
@ -93,6 +95,9 @@ export const getConfigsFromEnvVars = (): INJECTED_CONFIGS => {
|
|||
browserAgentlicenseKey:
|
||||
process.env.APPSMITH_NEW_RELIC_BROWSER_AGENT_LICENSE_KEY || "",
|
||||
otlpLicenseKey: process.env.APPSMITH_NEW_RELIC_OTLP_LICENSE_KEY || "",
|
||||
otlpEndpoint: process.env.APPSMITH_NEW_RELIC_OTEL_SERVICE_NAME || "",
|
||||
otlpServiceName:
|
||||
process.env.APPSMITH_NEW_RELIC_OTEL_EXPORTER_OTLP_ENDPOINT || "",
|
||||
},
|
||||
logLevel:
|
||||
(process.env.REACT_APP_CLIENT_LOG_LEVEL as
|
||||
|
|
@ -177,6 +182,14 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
|
|||
APPSMITH_FEATURE_CONFIGS?.newRelic.otlpLicenseKey,
|
||||
);
|
||||
|
||||
const newRelicOtlpServiceName = getConfig(
|
||||
ENV_CONFIG.newRelic.otlpServiceName,
|
||||
APPSMITH_FEATURE_CONFIGS?.newRelic.otlpServiceName,
|
||||
);
|
||||
const newRelicOtlpEndpoint = getConfig(
|
||||
ENV_CONFIG.newRelic.otlpEndpoint,
|
||||
APPSMITH_FEATURE_CONFIGS?.newRelic.otlpEndpoint,
|
||||
);
|
||||
const fusioncharts = getConfig(
|
||||
ENV_CONFIG.fusioncharts.licenseKey,
|
||||
APPSMITH_FEATURE_CONFIGS?.fusioncharts.licenseKey,
|
||||
|
|
@ -257,6 +270,8 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
|
|||
applicationId: newRelicApplicationId.value,
|
||||
browserAgentlicenseKey: newRelicBrowserLicenseKey.value,
|
||||
otlpLicenseKey: newRelicOtlpLicenseKey.value,
|
||||
otlpEndpoint: newRelicOtlpEndpoint.value,
|
||||
otlpServiceName: newRelicOtlpServiceName.value,
|
||||
},
|
||||
fusioncharts: {
|
||||
enabled: fusioncharts.enabled,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export interface AppsmithUIConfigs {
|
|||
applicationId: string;
|
||||
browserAgentlicenseKey: string;
|
||||
otlpLicenseKey: string;
|
||||
otlpServiceName: string;
|
||||
otlpEndpoint: string;
|
||||
};
|
||||
segment: {
|
||||
enabled: boolean;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ enableNewRelic &&
|
|||
(async () => {
|
||||
try {
|
||||
await import(
|
||||
/* webpackChunkName: "otlpTelemetry" */ "./auto-otel-web.js"
|
||||
/* webpackChunkName: "otlpTelemetry" */ "./UITelemetry/auto-otel-web.js"
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("Error loading telemetry script", e);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import type { updateActionDataPayloadType } from "actions/pluginActionActions";
|
||||
import {
|
||||
clearActionResponse,
|
||||
executePluginActionError,
|
||||
|
|
@ -162,6 +163,12 @@ import {
|
|||
} from "@appsmith/selectors/environmentSelectors";
|
||||
import { EVAL_WORKER_ACTIONS } from "@appsmith/workers/Evaluation/evalWorkerActions";
|
||||
import { getIsActionCreatedInApp } from "@appsmith/utils/getIsActionCreatedInApp";
|
||||
import type { OtlpSpan } from "UITelemetry/generateTraces";
|
||||
import {
|
||||
endSpan,
|
||||
setAttributesToSpan,
|
||||
startRootSpan,
|
||||
} from "UITelemetry/generateTraces";
|
||||
|
||||
enum ActionResponseDataTypes {
|
||||
BINARY = "BINARY",
|
||||
|
|
@ -498,6 +505,7 @@ export default function* executePluginActionTriggerSaga(
|
|||
pluginAction: TRunDescription,
|
||||
eventType: EventType,
|
||||
) {
|
||||
const span = startRootSpan("executePluginActionTriggerSaga");
|
||||
const { payload: pluginPayload } = pluginAction;
|
||||
const { actionId, onError, params } = pluginPayload;
|
||||
if (getType(params) !== Types.OBJECT) {
|
||||
|
|
@ -515,6 +523,10 @@ export default function* executePluginActionTriggerSaga(
|
|||
},
|
||||
actionId,
|
||||
);
|
||||
span &&
|
||||
setAttributesToSpan(span, {
|
||||
actionId: actionId,
|
||||
});
|
||||
const appMode: APP_MODE | undefined = yield select(getAppMode);
|
||||
const action = shouldBeDefined<Action>(
|
||||
yield select(getAction, actionId),
|
||||
|
|
@ -564,6 +576,8 @@ export default function* executePluginActionTriggerSaga(
|
|||
action,
|
||||
pagination,
|
||||
params,
|
||||
undefined,
|
||||
span,
|
||||
);
|
||||
const { isError, payload } = executePluginActionResponse;
|
||||
|
||||
|
|
@ -742,6 +756,7 @@ export function* runActionSaga(
|
|||
action?: Action;
|
||||
}>,
|
||||
) {
|
||||
const span = startRootSpan("runActionSaga");
|
||||
const actionId = reduxAction.payload.id;
|
||||
const isSaving: boolean = yield select(isActionSaving(actionId));
|
||||
const isDirty: boolean = yield select(isActionDirty(actionId));
|
||||
|
|
@ -810,6 +825,7 @@ export function* runActionSaga(
|
|||
paginationField,
|
||||
{},
|
||||
true,
|
||||
span,
|
||||
);
|
||||
payload = executePluginActionResponse.payload;
|
||||
isError = executePluginActionResponse.isError;
|
||||
|
|
@ -1075,7 +1091,7 @@ function* executeOnPageLoadJSAction(pageAction: PageAction) {
|
|||
}
|
||||
}
|
||||
|
||||
function* executePageLoadAction(pageAction: PageAction) {
|
||||
function* executePageLoadAction(pageAction: PageAction, span?: OtlpSpan) {
|
||||
const currentEnvDetails: { id: string; name: string } = yield select(
|
||||
getCurrentEnvironmentDetails,
|
||||
);
|
||||
|
|
@ -1123,7 +1139,14 @@ function* executePageLoadAction(pageAction: PageAction) {
|
|||
|
||||
try {
|
||||
const executePluginActionResponse: ExecutePluginActionResponse =
|
||||
yield call(executePluginActionSaga, action);
|
||||
yield call(
|
||||
executePluginActionSaga,
|
||||
action,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
span,
|
||||
);
|
||||
payload = executePluginActionResponse.payload;
|
||||
isError = executePluginActionResponse.isError;
|
||||
} catch (e) {
|
||||
|
|
@ -1250,13 +1273,14 @@ function* executePageLoadAction(pageAction: PageAction) {
|
|||
}
|
||||
|
||||
function* executePageLoadActionsSaga() {
|
||||
const span = startRootSpan("executePageLoadActionsSaga");
|
||||
try {
|
||||
const pageActions: PageAction[][] = yield select(getLayoutOnLoadActions);
|
||||
const layoutOnLoadActionErrors: LayoutOnLoadActionErrors[] = yield select(
|
||||
getLayoutOnLoadIssues,
|
||||
);
|
||||
const actionCount = flatten(pageActions).length;
|
||||
|
||||
span && setAttributesToSpan(span, { numActions: actionCount });
|
||||
// when cyclical depedency issue is there,
|
||||
// none of the page load actions would be executed
|
||||
PerformanceTracker.startAsyncTracking(
|
||||
|
|
@ -1267,7 +1291,9 @@ function* executePageLoadActionsSaga() {
|
|||
// Load all sets in parallel
|
||||
// @ts-expect-error: no idea how to type this
|
||||
yield* yield all(
|
||||
actionSet.map((apiAction) => call(executePageLoadAction, apiAction)),
|
||||
actionSet.map((apiAction) =>
|
||||
call(executePageLoadAction, apiAction, span),
|
||||
),
|
||||
);
|
||||
}
|
||||
PerformanceTracker.stopAsyncTracking(
|
||||
|
|
@ -1284,6 +1310,7 @@ function* executePageLoadActionsSaga() {
|
|||
kind: "error",
|
||||
});
|
||||
}
|
||||
endSpan(span);
|
||||
}
|
||||
|
||||
interface ExecutePluginActionResponse {
|
||||
|
|
@ -1301,9 +1328,14 @@ function* executePluginActionSaga(
|
|||
paginationField?: PaginationField,
|
||||
params?: Record<string, unknown>,
|
||||
isUserInitiated?: boolean,
|
||||
parentSpan?: OtlpSpan,
|
||||
) {
|
||||
const actionId = pluginAction.id;
|
||||
|
||||
parentSpan &&
|
||||
setAttributesToSpan(parentSpan, {
|
||||
actionId,
|
||||
pluginName: pluginAction?.name,
|
||||
});
|
||||
if (pluginAction.confirmBeforeExecute) {
|
||||
const modalPayload = {
|
||||
name: pluginAction.name,
|
||||
|
|
@ -1372,7 +1404,8 @@ function* executePluginActionSaga(
|
|||
let payload = EMPTY_RESPONSE;
|
||||
let response: ActionExecutionResponse;
|
||||
try {
|
||||
response = yield ActionAPI.executeAction(formData, timeout);
|
||||
response = yield ActionAPI.executeAction(formData, timeout, parentSpan);
|
||||
|
||||
const isError = isErrorResponse(response);
|
||||
PerformanceTracker.stopAsyncTracking(
|
||||
PerformanceTransactionName.EXECUTE_ACTION,
|
||||
|
|
@ -1389,13 +1422,16 @@ function* executePluginActionSaga(
|
|||
);
|
||||
|
||||
yield put(
|
||||
updateActionData([
|
||||
{
|
||||
entityName: pluginAction.name,
|
||||
dataPath: "data",
|
||||
data: isError ? undefined : payload.body,
|
||||
},
|
||||
]),
|
||||
updateActionData(
|
||||
[
|
||||
{
|
||||
entityName: pluginAction.name,
|
||||
dataPath: "data",
|
||||
data: isError ? undefined : payload.body,
|
||||
},
|
||||
],
|
||||
parentSpan,
|
||||
),
|
||||
);
|
||||
// TODO: Plugins are not always fetched before on page load actions are executed.
|
||||
try {
|
||||
|
|
@ -1451,13 +1487,16 @@ function* executePluginActionSaga(
|
|||
}),
|
||||
);
|
||||
yield put(
|
||||
updateActionData([
|
||||
{
|
||||
entityName: pluginAction.name,
|
||||
dataPath: "data",
|
||||
data: EMPTY_RESPONSE.body,
|
||||
},
|
||||
]),
|
||||
updateActionData(
|
||||
[
|
||||
{
|
||||
entityName: pluginAction.name,
|
||||
dataPath: "data",
|
||||
data: EMPTY_RESPONSE.body,
|
||||
},
|
||||
],
|
||||
parentSpan,
|
||||
),
|
||||
);
|
||||
if (e instanceof UserCancelledActionExecutionError) {
|
||||
// Case: user cancelled the request of file upload
|
||||
|
|
@ -1587,18 +1626,15 @@ function* softRefreshActionsSaga() {
|
|||
}
|
||||
|
||||
function* handleUpdateActionData(
|
||||
action: ReduxAction<{
|
||||
entityName: string;
|
||||
dataPath: string;
|
||||
data: unknown;
|
||||
dataPathRef?: string;
|
||||
}>,
|
||||
action: ReduxAction<updateActionDataPayloadType>,
|
||||
) {
|
||||
const { actionDataPayload, parentSpan } = action.payload;
|
||||
yield call(
|
||||
evalWorker.request,
|
||||
EVAL_WORKER_ACTIONS.UPDATE_ACTION_DATA,
|
||||
action.payload,
|
||||
actionDataPayload,
|
||||
);
|
||||
endSpan(parentSpan);
|
||||
}
|
||||
|
||||
export function* watchPluginActionExecutionSagas() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { uniqueId } from "lodash";
|
|||
import log from "loglevel";
|
||||
import type { TMessage } from "./MessageUtil";
|
||||
import { MessageType, sendMessage } from "./MessageUtil";
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { endSpan, startRootSpan } from "UITelemetry/generateTraces";
|
||||
|
||||
/**
|
||||
* Wrap a webworker to provide a synchronous request-response semantic.
|
||||
|
|
@ -165,11 +165,9 @@ export class GracefulWorkerService {
|
|||
this._channels.set(messageId, ch);
|
||||
const mainThreadStartTime = performance.now();
|
||||
let timeTaken;
|
||||
const span = startRootSpan(method);
|
||||
|
||||
try {
|
||||
const tracer = trace.getTracer("eval");
|
||||
const span = tracer?.startSpan(method);
|
||||
|
||||
sendMessage.call(this._Worker, {
|
||||
messageType: MessageType.REQUEST,
|
||||
body: {
|
||||
|
|
@ -181,11 +179,12 @@ export class GracefulWorkerService {
|
|||
|
||||
// The `this._broker` method is listening to events and will pass response to us over this channel.
|
||||
const response = yield take(ch);
|
||||
span?.end();
|
||||
timeTaken = response.timeTaken;
|
||||
const { data: responseData } = response;
|
||||
return responseData;
|
||||
} finally {
|
||||
endSpan(span);
|
||||
|
||||
// Log perf of main thread and worker
|
||||
const mainThreadEndTime = performance.now();
|
||||
const timeTakenOnMainThread = mainThreadEndTime - mainThreadStartTime;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface UpdateActionProps {
|
|||
export default function (request: EvalWorkerSyncRequest) {
|
||||
const actionsDataToUpdate: UpdateActionProps[] = request.data;
|
||||
handleActionsDataUpdate(actionsDataToUpdate);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleActionsDataUpdate(actionsToUpdate: UpdateActionProps[]) {
|
||||
|
|
|
|||
|
|
@ -30,10 +30,39 @@
|
|||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-bom</artifactId>
|
||||
<version>1.22.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-otlp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-otlp-logs</artifactId>
|
||||
<version>1.22.0-alpha</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
|
||||
<version>1.22.0-alpha</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
Ideally this dependency should have been added in the pom.xml file of GraphQLPlugin module, but it is
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
package com.appsmith.server;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import static com.appsmith.server.constants.EnvVariables.APPSMITH_NEW_RELIC_ACCOUNT_ENABLE;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class ServerApplication {
|
||||
|
|
@ -14,4 +20,16 @@ public class ServerApplication {
|
|||
.bannerMode(Banner.Mode.OFF)
|
||||
.run(args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenTelemetry openTelemetry() {
|
||||
String instanceName =
|
||||
StringUtils.defaultIfEmpty(System.getenv(String.valueOf(APPSMITH_NEW_RELIC_ACCOUNT_ENABLE)), null);
|
||||
// generate the bean only if the telemetry env variable is enabled
|
||||
|
||||
if ("true".equalsIgnoreCase(instanceName)) {
|
||||
return AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ public enum EnvVariables {
|
|||
APPSMITH_FORM_LOGIN_DISABLED,
|
||||
APPSMITH_ALLOWED_FRAME_ANCESTORS,
|
||||
APPSMITH_DISABLE_IFRAME_WIDGET_SANDBOX,
|
||||
APPSMITH_NEW_RELIC_ACCOUNT_ENABLE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.appsmith.server.controllers;
|
|||
|
||||
import com.appsmith.server.constants.Url;
|
||||
import com.appsmith.server.controllers.ce.ActionControllerCE;
|
||||
import com.appsmith.server.helpers.OtlpTelemetry;
|
||||
import com.appsmith.server.newactions.base.NewActionService;
|
||||
import com.appsmith.server.refactors.applications.RefactoringSolution;
|
||||
import com.appsmith.server.services.LayoutActionService;
|
||||
|
|
@ -19,8 +20,9 @@ public class ActionController extends ActionControllerCE {
|
|||
LayoutActionService layoutActionService,
|
||||
NewActionService newActionService,
|
||||
RefactoringSolution refactoringSolution,
|
||||
ActionExecutionSolution actionExecutionSolution) {
|
||||
ActionExecutionSolution actionExecutionSolution,
|
||||
OtlpTelemetry otlpTelemetry) {
|
||||
|
||||
super(layoutActionService, newActionService, refactoringSolution, actionExecutionSolution);
|
||||
super(layoutActionService, newActionService, refactoringSolution, actionExecutionSolution, otlpTelemetry);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import com.appsmith.server.dtos.EntityType;
|
|||
import com.appsmith.server.dtos.LayoutDTO;
|
||||
import com.appsmith.server.dtos.RefactorEntityNameDTO;
|
||||
import com.appsmith.server.dtos.ResponseDTO;
|
||||
import com.appsmith.server.helpers.OtlpTelemetry;
|
||||
import com.appsmith.server.newactions.base.NewActionService;
|
||||
import com.appsmith.server.refactors.applications.RefactoringSolution;
|
||||
import com.appsmith.server.services.LayoutActionService;
|
||||
import com.appsmith.server.solutions.ActionExecutionSolution;
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
|
@ -47,17 +49,20 @@ public class ActionControllerCE {
|
|||
private final NewActionService newActionService;
|
||||
private final RefactoringSolution refactoringSolution;
|
||||
private final ActionExecutionSolution actionExecutionSolution;
|
||||
private final OtlpTelemetry otlpTelemetry;
|
||||
|
||||
@Autowired
|
||||
public ActionControllerCE(
|
||||
LayoutActionService layoutActionService,
|
||||
NewActionService newActionService,
|
||||
RefactoringSolution refactoringSolution,
|
||||
ActionExecutionSolution actionExecutionSolution) {
|
||||
ActionExecutionSolution actionExecutionSolution,
|
||||
OtlpTelemetry otlpTelemetry) {
|
||||
this.layoutActionService = layoutActionService;
|
||||
this.newActionService = newActionService;
|
||||
this.refactoringSolution = refactoringSolution;
|
||||
this.actionExecutionSolution = actionExecutionSolution;
|
||||
this.otlpTelemetry = otlpTelemetry;
|
||||
}
|
||||
|
||||
@JsonView(Views.Public.class)
|
||||
|
|
@ -91,10 +96,14 @@ public class ActionControllerCE {
|
|||
public Mono<ResponseDTO<ActionExecutionResult>> executeAction(
|
||||
@RequestBody Flux<Part> partFlux,
|
||||
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName,
|
||||
@RequestHeader(name = FieldName.ENVIRONMENT_ID, required = false) String environmentId) {
|
||||
@RequestHeader(name = FieldName.ENVIRONMENT_ID, required = false) String environmentId,
|
||||
@RequestHeader(value = OtlpTelemetry.OTLP_HEADER_KEY, required = false) String traceparent) {
|
||||
Span span = this.otlpTelemetry.startOtlpSpanFromTraceparent("action service execute", traceparent);
|
||||
|
||||
return actionExecutionSolution
|
||||
.executeAction(partFlux, branchName, environmentId)
|
||||
.map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null));
|
||||
.map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null))
|
||||
.doFinally(signalType -> this.otlpTelemetry.endOtlpSpanSafely(span));
|
||||
}
|
||||
|
||||
@JsonView(Views.Public.class)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package com.appsmith.server.helpers;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.context.propagation.TextMapGetter;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
class ExtractModel {
|
||||
|
||||
private Map<String, String> headers;
|
||||
|
||||
public void addHeader(String key, String value) {
|
||||
if (this.headers == null) {
|
||||
headers = new HashMap<>();
|
||||
}
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
public class OtlpTelemetry {
|
||||
private Tracer tracer = null;
|
||||
public static final String OTLP_HEADER_KEY = "traceparent-otlp";
|
||||
|
||||
@Autowired
|
||||
public OtlpTelemetry(@Nullable OpenTelemetry openTelemetry) {
|
||||
|
||||
if (openTelemetry != null) {
|
||||
this.tracer = openTelemetry.getTracer("Server");
|
||||
}
|
||||
}
|
||||
|
||||
// when traces are initiated from the client side, its context is embedded in the traceparent request header
|
||||
// we use the function below to build the client's trace context
|
||||
private Context generateContextFromTraceHeader(String traceparent) {
|
||||
TextMapGetter<ExtractModel> getter = new TextMapGetter<>() {
|
||||
@Override
|
||||
public String get(ExtractModel carrier, String key) {
|
||||
if (carrier.getHeaders().containsKey(key)) {
|
||||
return carrier.getHeaders().get(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> keys(ExtractModel carrier) {
|
||||
return carrier.getHeaders().keySet();
|
||||
}
|
||||
};
|
||||
ExtractModel model = new ExtractModel();
|
||||
model.addHeader("traceparent", traceparent);
|
||||
|
||||
return W3CTraceContextPropagator.getInstance().extract(Context.current(), model, getter);
|
||||
}
|
||||
|
||||
private Span startOTLPSpan(String spanName, Context context) {
|
||||
return tracer.spanBuilder(spanName)
|
||||
.setSpanKind(SpanKind.SERVER)
|
||||
.setParent(context)
|
||||
.startSpan();
|
||||
}
|
||||
// we build traces using the client's trace context as the parent context, So that any other spans generated
|
||||
// from the server appear as a subspan of the client
|
||||
public Span startOtlpSpanFromTraceparent(String spanName, String traceparent) {
|
||||
if (this.tracer == null) {
|
||||
return null;
|
||||
}
|
||||
if (isBlank(spanName) || isBlank(traceparent)) {
|
||||
return null;
|
||||
}
|
||||
Context context = generateContextFromTraceHeader(traceparent);
|
||||
return startOTLPSpan(spanName, context);
|
||||
}
|
||||
|
||||
public void endOtlpSpanSafely(Span span) {
|
||||
if (span != null) {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user