chore: frontend and backend telemetry updates for execute flow #28800 and #28805 (#28936)

## 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:
Vemparala Surya Vamsi 2023-11-24 13:09:02 +05:30 committed by GitHub
parent dea2fd736c
commit 2f6f824efc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 409 additions and 55 deletions

View File

@ -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}';
}

View File

@ -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__"),

View File

@ -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__"),

View File

@ -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({

View 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;

View File

@ -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,
},
};
};

View File

@ -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,

View File

@ -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,

View File

@ -31,6 +31,8 @@ export interface AppsmithUIConfigs {
applicationId: string;
browserAgentlicenseKey: string;
otlpLicenseKey: string;
otlpServiceName: string;
otlpEndpoint: string;
};
segment: {
enabled: boolean;

View File

@ -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);

View File

@ -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() {

View File

@ -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;

View File

@ -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[]) {

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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();
}
}
}