mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
refactor(server): reorganize server configs (#5753)
This commit is contained in:
@@ -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: {
|
||||
/**
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
applyEnvToConfig,
|
||||
Config,
|
||||
type ConfigPaths,
|
||||
DeploymentType,
|
||||
getDefaultAFFiNEStorageConfig,
|
||||
} from './config';
|
||||
export * from './error';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user