1825 extensibility v1 see serverless logs using subscriptions in twnty cli or settings serverkess section (#16321)

- Adds a log section to settings serverless functions test tab

<img width="1303" height="827" alt="image"
src="https://github.com/user-attachments/assets/2ce70558-91bc-4cfc-aced-42ac9e0226bf"
/>

- Adds a new subscription endpoint to graphql api
`serverlessFunctionLogs` that that emit function logs

- Adds a new command `twenty app logs` to `twenty-sdk` to watch logs in
terminal

<img width="1109" height="182" alt="image"
src="https://github.com/user-attachments/assets/2e874060-e99d-4978-ab9c-c91e52cb7478"
/>


<img width="1061" height="176" alt="image"
src="https://github.com/user-attachments/assets/1f533e32-7435-4d7b-89c6-d1154816a8ab"
/>

- add new version for create-twenty-app and twenty-sdk `0.1.3`
This commit is contained in:
martmull
2025-12-04 16:40:58 +01:00
committed by GitHub
parent d1befa7e35
commit 5e8fbc7c5c
22 changed files with 339 additions and 20 deletions
@@ -0,0 +1 @@
export const SERVERLESS_FUNCTION_LOGS_TRIGGER = 'serverlessFunctionLogs';
@@ -0,0 +1,22 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
@ObjectType('ServerlessFunctionLogs')
export class ServerlessFunctionLogsDTO {
@Field({ description: 'Execution Logs' })
logs: string;
@HideField()
applicationUniversalIdentifier?: string;
@HideField()
applicationId?: string;
@HideField()
name?: string;
@HideField()
id?: string;
@HideField()
universalIdentifier?: string;
}
@@ -0,0 +1,21 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType('ServerlessFunctionLogsInput')
export class ServerlessFunctionLogsInput {
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@Field(() => UUIDScalarType, { nullable: true })
applicationUniversalIdentifier?: string;
@Field(() => String, { nullable: true })
name?: string;
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
}
@@ -23,6 +23,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
@Module({
imports: [
@@ -43,6 +44,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
ServerlessFunctionLayerModule,
SubscriptionsModule,
],
providers: [
ServerlessFunctionService,
@@ -1,9 +1,11 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Inject, UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import graphqlTypeJson from 'graphql-type-json';
import { Repository } from 'typeorm';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { isDefined } from 'twenty-shared/utils';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -24,6 +26,9 @@ import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serve
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
import { ServerlessFunctionLogsDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.dto';
import { ServerlessFunctionLogsInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.input';
import { SERVERLESS_FUNCTION_LOGS_TRIGGER } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-logs-trigger';
@UseGuards(
WorkspaceAuthGuard,
@@ -38,6 +43,8 @@ export class ServerlessFunctionResolver {
private readonly serverlessFunctionService: ServerlessFunctionService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@Inject('PUB_SUB')
private readonly pubSub: RedisPubSub,
) {}
@Query(() => ServerlessFunctionDTO)
@@ -184,4 +191,42 @@ export class ServerlessFunctionResolver {
serverlessFunctionGraphQLApiExceptionHandler(error);
}
}
@Subscription(() => ServerlessFunctionLogsDTO, {
filter: (
payload: { serverlessFunctionLogs: ServerlessFunctionLogsDTO },
variables: { input: ServerlessFunctionLogsInput },
) => {
const { serverlessFunctionLogs } = payload;
const {
id,
universalIdentifier,
applicationId,
applicationUniversalIdentifier,
name,
} = serverlessFunctionLogs;
const {
id: inputId,
universalIdentifier: inputUniversalIdentifier,
name: inputName,
applicationId: inputApplicationId,
applicationUniversalIdentifier: inputApplicationUniversalIdentifier,
} = variables.input;
return (
(!isDefined(inputId) || inputId === id) &&
(!isDefined(inputUniversalIdentifier) ||
inputUniversalIdentifier === universalIdentifier) &&
(!isDefined(inputName) || inputName === name) &&
(!isDefined(inputApplicationId) ||
inputApplicationId === applicationId) &&
(!isDefined(inputApplicationUniversalIdentifier) ||
inputApplicationUniversalIdentifier ===
applicationUniversalIdentifier)
);
},
})
serverlessFunctionLogs(@Args('input') _: ServerlessFunctionLogsInput) {
return this.pubSub.asyncIterator(SERVERLESS_FUNCTION_LOGS_TRIGGER);
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { join } from 'path';
@@ -6,6 +6,7 @@ import { join } from 'path';
import deepEqual from 'deep-equal';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
@@ -31,6 +32,7 @@ import {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
import { SERVERLESS_FUNCTION_LOGS_TRIGGER } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-logs-trigger';
@Injectable()
export class ServerlessFunctionService {
@@ -43,6 +45,8 @@ export class ServerlessFunctionService {
private readonly throttlerService: ThrottlerService,
private readonly twentyConfigService: TwentyConfigService,
private readonly auditService: AuditService,
@Inject('PUB_SUB')
private readonly pubSub: RedisPubSub,
) {}
async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) {
@@ -98,6 +102,7 @@ export class ServerlessFunctionService {
},
relations: [
'serverlessFunctionLayer',
'application',
'application.applicationVariables',
],
});
@@ -113,6 +118,18 @@ export class ServerlessFunctionService {
console.log(resultServerlessFunction.logs);
}
await this.pubSub.publish(SERVERLESS_FUNCTION_LOGS_TRIGGER, {
serverlessFunctionLogs: {
logs: resultServerlessFunction.logs,
id: functionToExecute.id,
name: functionToExecute.name,
universalIdentifier: functionToExecute.universalIdentifier,
applicationId: functionToExecute.applicationId,
applicationUniversalIdentifier:
functionToExecute.application?.universalIdentifier,
},
});
this.auditService
.createContext({
workspaceId,