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

PoC on usage of Apm for performance analysis #4329

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,361 changes: 1,318 additions & 43 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"dataloader": "^2.2.2",
"elastic-apm-node": "^4.7.0",
"file-type": "^19.1.1",
"graphql": "^16.9.0",
"graphql-amqp-subscriptions": "^2.0.0",
Expand Down
7 changes: 7 additions & 0 deletions src/apm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const apm = require('elastic-apm-node').start({
apiKey: '',
serverUrl: '',
verifyServerCert: false,
environment: 'local',
});
24 changes: 22 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { ValidationPipe } from '@common/pipes/validation.pipe';
import configuration from '@config/configuration';
import {
configQuery,
spacesQuery,
meQuery,
platformMetadataQuery,
spacesQuery,
} from '@config/graphql';
import { AuthenticationModule } from '@core/authentication/authentication.module';
import { AuthorizationModule } from '@core/authorization/authorization.module';
Expand Down Expand Up @@ -56,8 +56,8 @@ import { SsiCredentialFlowModule } from '@services/api-rest/ssi-credential-flow/
import { StorageAccessModule } from '@services/api-rest/storage-access/storage.access.module';
import { MessageReactionModule } from '@domain/communication/message.reaction/message.reaction.module';
import {
HttpExceptionFilter,
GraphqlExceptionFilter,
HttpExceptionFilter,
UnhandledExceptionFilter,
} from '@core/error-handling';
import { MeModule } from '@services/api/me';
Expand All @@ -78,6 +78,10 @@ import { PlatformSettingsModule } from '@platform/settings/platform.settings.mod
import { FileIntegrationModule } from '@services/file-integration';
import { AdminLicensingModule } from '@platform/admin/licensing/admin.licensing.module';
import { PlatformRoleModule } from '@platform/platfrom.role/platform.role.module';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import type { GraphQLRequestContextDidResolveOperation } from '@apollo/server/dist/externalTypes/requestPipeline';
import { apm } from './apm';

@Module({
imports: [
Expand Down Expand Up @@ -173,6 +177,22 @@ import { PlatformRoleModule } from '@platform/platfrom.role/platform.role.module
],
},
fieldResolverEnhancers: ['guards', 'filters'],
plugins: [
{
async requestDidStart() {
return {
async didResolveOperation(
requestContext: GraphQLRequestContextDidResolveOperation<any>
) {
apm.currentTransaction.name =
requestContext.operationName ?? 'Unnamed';
apm.currentTransaction.type =
requestContext.operation.operation;
},
};
},
},
],
sortSchema: true,
persistedQueries: false,
/***
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { apm } from '@src/apm';

export const createInstrumentMethodDecorator = (type: string) => () => {
return (
targetClass: any,
methodName: string,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = new Proxy(originalMethod, {
apply(target: any, thisArg: any, argArray: any[]): any {
const span = apm.currentTransaction.startSpan(methodName, type);
const func = Reflect.apply(target, thisArg, argArray);
const isPromise = func instanceof Promise;
const isFunction = func instanceof Function;
// start span
// execute and measure
if (isPromise) {
return (func as PromiseLike<any>).then(x => {
span.end();
return x;
});
} else if (isFunction) {
span.end();
return Reflect.apply(func, thisArg, argArray);
} else {
span.end();
return func;
}
},
});

return descriptor;
};
};
4 changes: 4 additions & 0 deletions src/common/decorators/instrumentation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './instrument.field.decorator';
export * from './instrument.query.decorator';
export * from './instrument.service.decorator';
export * from './instrument.mutation.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createInstrumentMethodDecorator } from './create.instrument.method.decorator';

export const InstrumentField =
createInstrumentMethodDecorator('resolver-field');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createInstrumentMethodDecorator } from './create.instrument.method.decorator';

export const InstrumentMutation =
createInstrumentMethodDecorator('resolver-mutation');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createInstrumentMethodDecorator } from './create.instrument.method.decorator';

export const InstrumentQuery =
createInstrumentMethodDecorator('resolver-query');
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { apm } from 'src/apm';

// eslint-disable-next-line @typescript-eslint/ban-types
export function InstrumentService(target: Function) {
for (const methodName of Object.getOwnPropertyNames(target.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(
target.prototype,
methodName
);
// if descriptor is not found for some reason
if (!descriptor) {
continue;
}
// skip if not a method call
if (!(descriptor.value instanceof Function)) {
continue;
}

const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!apm.currentTransaction) {
return originalMethod.apply(this, args);
}

const span = apm.currentTransaction.startSpan(
`${target.name}.${methodName}`,
'service-call'
);

const value = originalMethod.apply(this, args);

span.end();

return value;
};
Object.defineProperty(target.prototype, methodName, descriptor);
}
}
2 changes: 2 additions & 0 deletions src/core/authorization/authorization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { IAuthorizationPolicyRuleVerifiedCredential } from './authorization.poli
import { AuthorizationInvalidPolicyException } from '@common/exceptions/authorization.invalid.policy.exception';
import { IAuthorizationPolicyRulePrivilege } from './authorization.policy.rule.privilege.interface';
import { ForbiddenAuthorizationPolicyException } from '@common/exceptions/forbidden.authorization.policy.exception';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class AuthorizationService {
constructor(
Expand Down
16 changes: 12 additions & 4 deletions src/core/error-handling/unhandled.exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,20 @@ export class UnhandledExceptionFilter implements ExceptionFilter {
/* add values that you want to include as additional data
e.g. secondParam = { code: '123' };
*/
const secondParam = { errorId: randomUUID() };
const thirdParam = undefined;
const errorId = randomUUID();
const secondParam = exception.stack;
const thirdParam = 'UnhandledException';
/* the logger will handle the passed exception by iteration over all it's fields
* you can provide additional data in the stack and context
*/
this.logger.error(exception, secondParam, thirdParam);
this.logger.error(
{
...exception,
errorId,
},
secondParam,
thirdParam
);

const contextType = host.getType<ContextTypeWithGraphQL>();
// If we are in an http context respond something so the browser doesn't stay hanging.
Expand All @@ -36,7 +44,7 @@ export class UnhandledExceptionFilter implements ExceptionFilter {
response.status(500).json({
statusCode: 500,
timestamp: new Date().toISOString(),
errorId: secondParam.errorId,
errorId,
name:
process.env.NODE_ENV !== 'production' ? exception.name : undefined,
message:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
CREDENTIAL_RULE_CONTRIBUTION_CREATED_BY_DELETE,
} from '@common/constants';
import { LinkAuthorizationService } from '../link/link.service.authorization';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class CalloutContributionAuthorizationService {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { ValidationException } from '@common/exceptions';
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
import { LinkService } from '../link/link.service';
import { ILink } from '../link/link.interface';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class CalloutContributionService {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ICalloutContribution } from '../callout-contribution/callout.contributi
import { CalloutContributionAuthorizationService } from '../callout-contribution/callout.contribution.service.authorization';
import { CalloutContributionService } from '../callout-contribution/callout.contribution.service';
import { ILink } from '../link/link.interface';
import { InstrumentMutation } from '@common/decorators/instrumentation';

@Resolver()
export class CalloutResolverMutations {
Expand Down Expand Up @@ -113,9 +114,8 @@ export class CalloutResolverMutations {
`update visibility on callout: ${callout.id}`
);
const oldVisibility = callout.visibility;
const savedCallout = await this.calloutService.updateCalloutVisibility(
calloutData
);
const savedCallout =
await this.calloutService.updateCalloutVisibility(calloutData);

if (savedCallout.visibility !== oldVisibility) {
if (savedCallout.visibility === CalloutVisibility.PUBLISHED) {
Expand Down Expand Up @@ -176,6 +176,7 @@ export class CalloutResolverMutations {
description: 'Create a new Contribution on the Callout.',
})
@Profiling.api
@InstrumentMutation()
async createContributionOnCallout(
@CurrentUser() agentInfo: AgentInfo,
@Args('contributionData') contributionData: CreateContributionOnCalloutInput
Expand Down Expand Up @@ -263,7 +264,7 @@ export class CalloutResolverMutations {
}
}

return await this.calloutContributionService.save(contribution);
return this.calloutContributionService.save(contribution);
}

private async processActivityLinkCreated(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/collaboration/callout/callout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import { ICalloutContributionDefaults } from '../callout-contribution-defaults/c
import { CalloutContributionFilterArgs } from '../callout-contribution/dto/callout.contribution.args.filter';
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class CalloutService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/collaboration/link/link.service.authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authoriz
import { AuthorizationPrivilege } from '@common/enums/authorization.privilege';
import { AuthorizationCredential } from '@common/enums/authorization.credential';
import { CREDENTIAL_RULE_LINK_CREATED_BY } from '@common/constants/authorization/credential.rule.constants';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class LinkAuthorizationService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/collaboration/link/link.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { IProfile } from '@domain/common/profile/profile.interface';
import { ProfileType } from '@common/enums';
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class LinkService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/collaboration/post/post.service.authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { ProfileAuthorizationService } from '@domain/common/profile/profile.serv
import { RoomAuthorizationService } from '@domain/communication/room/room.service.authorization';
import { CommunityRole } from '@common/enums/community.role';
import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class PostAuthorizationService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/collaboration/post/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { RoomService } from '@domain/communication/room/room.service';
import { RoomType } from '@common/enums/room.type';
import { TagsetReservedName } from '@common/enums/tagset.reserved.name';
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class PostService {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import { AuthorizationPolicyRuleVerifiedCredential } from '@core/authorization/a
import { IAuthorizationPolicyRulePrivilege } from '@core/authorization/authorization.policy.rule.privilege.interface';
import { IAuthorizationPolicyRuleVerifiedCredential } from '@core/authorization/authorization.policy.rule.verified.credential.interface';
import { ICredentialDefinition } from '@domain/agent/credential/credential.definition.interface';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class AuthorizationPolicyService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/common/profile/profile.service.authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { VisualAuthorizationService } from '../visual/visual.service.authorizati
import { StorageBucketAuthorizationService } from '@domain/storage/storage-bucket/storage.bucket.service.authorization';
import { LogContext } from '@common/enums/logging.context';
import { RelationshipNotFoundException } from '@common/exceptions';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class ProfileAuthorizationService {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
import { ProfileAuthorizationService } from '../profile/profile.service.authorization';
import { IWhiteboard } from './whiteboard.interface';
import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class WhiteboardAuthorizationService {
constructor(
Expand Down
2 changes: 2 additions & 0 deletions src/domain/common/whiteboard/whiteboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import { LicenseEngineService } from '@core/license-engine/license.engine.servic
import { LicensePrivilege } from '@common/enums/license.privilege';
import { SubscriptionPublishService } from '@services/subscriptions/subscription-service';
import { isEqual } from 'lodash';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class WhiteboardService {
// The eventEmitter is used for cross-service communication.
Expand Down
12 changes: 6 additions & 6 deletions src/domain/storage/storage-bucket/storage.bucket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import { IStorageBucketParent } from './dto/storage.bucket.dto.parent';
import { UrlGeneratorService } from '@services/infrastructure/url-generator/url.generator.service';
import { ProfileType } from '@common/enums';
import { StorageUploadFailedException } from '@common/exceptions/storage/storage.upload.failed.exception';
import { InstrumentService } from '@common/decorators/instrumentation';

@InstrumentService
@Injectable()
export class StorageBucketService {
DEFAULT_MAX_ALLOWED_FILE_SIZE = 15728640;
Expand Down Expand Up @@ -188,9 +190,8 @@ export class StorageBucketService {
/* just consume */
}

const document = await this.documentService.createDocument(
createDocumentInput
);
const document =
await this.documentService.createDocument(createDocumentInput);
document.storageBucket = storage;

this.logger.verbose?.(
Expand All @@ -215,9 +216,8 @@ export class StorageBucketService {
LogContext.DOCUMENT
);

const documentForReference = await this.documentService.getDocumentFromURL(
uri
);
const documentForReference =
await this.documentService.getDocumentFromURL(uri);

try {
const newDocument = await this.uploadFileAsDocument(
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { json } from 'body-parser';
import cookieParser from 'cookie-parser';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { INestApplication } from '@nestjs/common';
import { apm } from './apm';

const bootstrap = async () => {
const app = await NestFactory.create(AppModule, {
Expand Down
Loading