refactor(server): reorganize server configs (#5753)

This commit is contained in:
liuyi
2024-02-02 08:32:06 +00:00
parent ee3d195811
commit bef266ae3b
36 changed files with 423 additions and 189 deletions

View File

@@ -18,18 +18,22 @@ export enum ExternalAccount {
firebase = 'firebase',
}
export type ServerFlavor =
| 'allinone'
| 'main'
// @deprecated
| 'graphql'
| 'sync'
| 'selfhosted';
export type ServerFlavor = 'allinone' | 'graphql' | 'sync';
export type AFFINE_ENV = 'dev' | 'beta' | 'production';
export type NODE_ENV = 'development' | 'test' | 'production';
export enum DeploymentType {
Affine = 'affine',
Selfhosted = 'selfhosted',
}
export type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
| 'ENV_MAP'
| 'version'
| 'type'
| 'isSelfhosted'
| 'flavor'
| 'env'
| 'affine'
@@ -63,27 +67,36 @@ export interface AFFiNEConfig {
*/
readonly version: string;
/**
* Deployment type, AFFiNE Cloud, or Selfhosted
*/
get type(): DeploymentType;
/**
* Fast detect whether currently deployed in a selfhosted environment
*/
get isSelfhosted(): boolean;
/**
* Server flavor
*/
get flavor(): {
type: string;
main: boolean;
graphql: boolean;
sync: boolean;
selfhosted: boolean;
};
/**
* Deployment environment
*/
readonly affineEnv: 'dev' | 'beta' | 'production';
readonly AFFINE_ENV: AFFINE_ENV;
/**
* alias to `process.env.NODE_ENV`
*
* @default 'production'
* @default 'development'
* @env NODE_ENV
*/
readonly env: string;
readonly NODE_ENV: NODE_ENV;
/**
* fast AFFiNE environment judge
@@ -101,6 +114,7 @@ export interface AFFiNEConfig {
dev: boolean;
test: boolean;
};
get deploy(): boolean;
/**
@@ -302,11 +316,11 @@ export interface AFFiNEConfig {
updatePollInterval: number;
/**
* Use JwstCodec to merge updates at the same time when merging using Yjs.
* Use `y-octo` to merge updates at the same time when merging using Yjs.
*
* This is an experimental feature, and aimed to check the correctness of JwstCodec.
*/
experimentalMergeWithJwstCodec: boolean;
experimentalMergeWithYOcto: boolean;
};
history: {
/**

View File

@@ -6,7 +6,14 @@ import { merge } from 'lodash-es';
import parse from 'parse-duration';
import pkg from '../../../package.json' assert { type: 'json' };
import type { AFFiNEConfig, ServerFlavor } from './def';
import {
type AFFINE_ENV,
AFFiNEConfig,
DeploymentType,
type NODE_ENV,
type ServerFlavor,
} from './def';
import { readEnv } from './env';
import { getDefaultAFFiNEStorageConfig } from './storage';
// Don't use this in production
@@ -46,40 +53,62 @@ const jwtKeyPair = (function () {
})();
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
let isHttps: boolean | null = null;
let flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
const NODE_ENV = readEnv<NODE_ENV>('NODE_ENV', 'development', [
'development',
'test',
'production',
]);
const AFFINE_ENV = readEnv<AFFINE_ENV>('AFFINE_ENV', 'dev', [
'dev',
'beta',
'production',
]);
const flavor = readEnv<ServerFlavor>('SERVER_FLAVOR', 'allinone', [
'allinone',
'graphql',
'sync',
]);
const deploymentType = readEnv<DeploymentType>(
'DEPLOYMENT_TYPE',
NODE_ENV === 'development'
? DeploymentType.Affine
: DeploymentType.Selfhosted,
Object.values(DeploymentType)
);
const isSelfhosted = deploymentType === DeploymentType.Selfhosted;
const defaultConfig = {
serverId: 'affine-nestjs-server',
serverName: flavor === 'selfhosted' ? 'Self-Host Cloud' : 'AFFiNE Cloud',
serverName: isSelfhosted ? 'Self-Host Cloud' : 'AFFiNE Cloud',
version: pkg.version,
get type() {
return deploymentType;
},
get isSelfhosted() {
return isSelfhosted;
},
get flavor() {
if (flavor === 'graphql') {
flavor = 'main';
}
return {
type: flavor,
main: flavor === 'main' || flavor === 'allinone',
graphql: flavor === 'graphql' || flavor === 'allinone',
sync: flavor === 'sync' || flavor === 'allinone',
selfhosted: flavor === 'selfhosted',
};
},
ENV_MAP: {},
affineEnv: 'dev',
AFFINE_ENV,
get affine() {
const env = this.affineEnv;
return {
canary: env === 'dev',
beta: env === 'beta',
stable: env === 'production',
canary: AFFINE_ENV === 'dev',
beta: AFFINE_ENV === 'beta',
stable: AFFINE_ENV === 'production',
};
},
env: process.env.NODE_ENV ?? 'development',
NODE_ENV,
get node() {
const env = this.env;
return {
prod: env === 'production',
dev: env === 'development',
test: env === 'test',
prod: NODE_ENV === 'production',
dev: NODE_ENV === 'development',
test: NODE_ENV === 'test',
};
},
get deploy() {
@@ -88,12 +117,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
featureFlags: {
earlyAccessPreview: false,
},
get https() {
return isHttps ?? !this.node.dev;
},
set https(value: boolean) {
isHttps = value;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
@@ -160,7 +184,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
manager: {
enableUpdateAutoMerging: flavor !== 'sync',
updatePollInterval: 3000,
experimentalMergeWithJwstCodec: false,
experimentalMergeWithYOcto: false,
},
history: {
interval: 1000 * 60 * 10 /* 10 mins */,

View File

@@ -48,3 +48,24 @@ export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
}
}
}
export function readEnv<T>(
env: string,
defaultValue: T,
availableValues?: T[]
) {
const value = process.env[env];
if (value === undefined) {
return defaultValue;
}
if (availableValues && !availableValues.includes(value as any)) {
throw new Error(
`Invalid value '${value}' for environment variable ${env}, expected one of [${availableValues.join(
', '
)}]`
);
}
return value as T;
}

View File

@@ -9,6 +9,7 @@ export {
applyEnvToConfig,
Config,
type ConfigPaths,
DeploymentType,
getDefaultAFFiNEStorageConfig,
} from './config';
export * from './error';

View File

@@ -1,28 +1,48 @@
import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import {
Global,
Module,
OnModuleDestroy,
OnModuleInit,
Provider,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Config, parseEnvValue } from '../config';
import { createSDK, registerCustomMetrics } from './opentelemetry';
import { Config } from '../config';
import {
LocalOpentelemetryFactory,
OpentelemetryFactory,
registerCustomMetrics,
} from './opentelemetry';
const factorProvider: Provider = {
provide: OpentelemetryFactory,
useFactory: (config: Config) => {
return config.metrics.enabled ? new LocalOpentelemetryFactory() : null;
},
inject: [Config],
};
@Global()
@Module({})
@Module({
providers: [factorProvider],
exports: [factorProvider],
})
export class MetricsModule implements OnModuleInit, OnModuleDestroy {
private sdk: NodeSDK | null = null;
constructor(private readonly config: Config) {}
constructor(private readonly ref: ModuleRef) {}
onModuleInit() {
if (
this.config.metrics.enabled &&
!parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean')
) {
this.sdk = createSDK();
const factor = this.ref.get(OpentelemetryFactory, { strict: false });
if (factor) {
this.sdk = factor.create();
this.sdk.start();
registerCustomMetrics();
}
}
async onModuleDestroy() {
if (this.config.metrics.enabled && this.sdk) {
if (this.sdk) {
await this.sdk.shutdown();
}
}
@@ -30,3 +50,4 @@ export class MetricsModule implements OnModuleInit, OnModuleDestroy {
export * from './metrics';
export * from './utils';
export { OpentelemetryFactory };

View File

@@ -1,6 +1,4 @@
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
import { OnModuleDestroy } from '@nestjs/common';
import { metrics } from '@opentelemetry/api';
import {
CompositePropagator,
@@ -18,16 +16,13 @@ import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'
import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io';
import { Resource } from '@opentelemetry/resources';
import {
ConsoleMetricExporter,
type MeterProvider,
MetricProducer,
MetricReader,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
SpanExporter,
TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace-node';
@@ -38,7 +33,7 @@ import { PrismaMetricProducer } from './prisma';
const { PrismaInstrumentation } = prismaInstrument;
abstract class OpentelemetryFactor {
export abstract class OpentelemetryFactory {
abstract getMetricReader(): MetricReader;
abstract getSpanExporter(): SpanExporter;
@@ -59,7 +54,7 @@ abstract class OpentelemetryFactor {
getResource() {
return new Resource({
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.affineEnv,
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.AFFINE_ENV,
[SemanticResourceAttributes.SERVICE_NAME]: AFFiNE.flavor.type,
[SemanticResourceAttributes.SERVICE_VERSION]: AFFiNE.version,
});
@@ -85,32 +80,20 @@ abstract class OpentelemetryFactor {
}
}
class GCloudOpentelemetryFactor extends OpentelemetryFactor {
override getResource(): Resource {
return super.getResource().merge(new GcpDetectorSync().detect());
export class LocalOpentelemetryFactory
extends OpentelemetryFactory
implements OnModuleDestroy
{
private readonly metricsExporter = new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
async onModuleDestroy() {
await this.metricsExporter.shutdown();
}
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exportIntervalMillis: 30000,
exportTimeoutMillis: 10000,
exporter: new MetricExporter({
prefix: 'custom.googleapis.com',
}),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new TraceExporter();
}
}
class LocalOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
return this.metricsExporter;
}
override getSpanExporter(): SpanExporter {
@@ -118,33 +101,6 @@ class LocalOpentelemetryFactor extends OpentelemetryFactor {
}
}
class DebugOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new ConsoleSpanExporter();
}
}
// TODO(@forehalo): make it configurable
export function createSDK() {
let factor: OpentelemetryFactor | null = null;
if (process.env.NODE_ENV === 'production') {
factor = new GCloudOpentelemetryFactor();
} else if (process.env.DEBUG_METRICS) {
factor = new DebugOpentelemetryFactor();
} else {
factor = new LocalOpentelemetryFactor();
}
return factor?.create();
}
function getMeterProvider() {
return metrics.getMeterProvider();
}