Fix backend Elasticsearch connectivity for local dev and app logs.
Auto port-forward using the registered cluster kubeconfig, default to loopback outside Kubernetes, route log APIs by app cluster, and document platform env settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,7 +30,7 @@ REGISTRY_PASSWORD=registry_secret
|
|||||||
|
|
||||||
# Central logging (Elasticsearch + Kibana)
|
# Central logging (Elasticsearch + Kibana)
|
||||||
# In-cluster backend: leave ELASTICSEARCH_HOST unset (uses elasticsearch.logging.svc.cluster.local).
|
# In-cluster backend: leave ELASTICSEARCH_HOST unset (uses elasticsearch.logging.svc.cluster.local).
|
||||||
# Local backend (npm run dev): API auto-runs kubectl port-forward when host is loopback
|
# Local backend (npm run dev): defaults to 127.0.0.1 and auto-runs kubectl port-forward to the default cluster.
|
||||||
# ELASTICSEARCH_HOST=127.0.0.1
|
# ELASTICSEARCH_HOST=127.0.0.1
|
||||||
# ELASTICSEARCH_PORT=9200
|
# ELASTICSEARCH_PORT=9200
|
||||||
# ELASTICSEARCH_AUTO_PORT_FORWARD=false
|
# ELASTICSEARCH_AUTO_PORT_FORWARD=false
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ backend:
|
|||||||
PLATFORM_CREATE_STORAGE_CLASS: "true"
|
PLATFORM_CREATE_STORAGE_CLASS: "true"
|
||||||
PLATFORM_STORAGE_CLASS: cloudhost-expandable
|
PLATFORM_STORAGE_CLASS: cloudhost-expandable
|
||||||
PLATFORM_STORAGE_PROVISIONER: rancher.io/local-path
|
PLATFORM_STORAGE_PROVISIONER: rancher.io/local-path
|
||||||
|
ELASTICSEARCH_HOST: elasticsearch.logging.svc.cluster.local
|
||||||
|
ELASTICSEARCH_AUTO_PORT_FORWARD: "false"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -36,8 +36,12 @@ export default () => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
elasticsearch: {
|
elasticsearch: {
|
||||||
/** API host for log search. Use cluster DNS in-cluster; 127.0.0.1 + port-forward when backend runs locally. */
|
/** API host for log search. In-cluster: cluster DNS. Local dev: loopback + auto port-forward. */
|
||||||
host: process.env.ELASTICSEARCH_HOST || 'elasticsearch.logging.svc.cluster.local',
|
host:
|
||||||
|
process.env.ELASTICSEARCH_HOST ||
|
||||||
|
(process.env.KUBERNETES_SERVICE_HOST
|
||||||
|
? 'elasticsearch.logging.svc.cluster.local'
|
||||||
|
: '127.0.0.1'),
|
||||||
port: parseInt(process.env.ELASTICSEARCH_PORT || '9200', 10),
|
port: parseInt(process.env.ELASTICSEARCH_PORT || '9200', 10),
|
||||||
/** In development with loopback host, start kubectl port-forward on API boot (set false to manage manually). */
|
/** In development with loopback host, start kubectl port-forward on API boot (set false to manage manually). */
|
||||||
autoPortForward: process.env.ELASTICSEARCH_AUTO_PORT_FORWARD ?? 'true',
|
autoPortForward: process.env.ELASTICSEARCH_AUTO_PORT_FORWARD ?? 'true',
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
private readonly KIBANA_NAME = 'kibana';
|
private readonly KIBANA_NAME = 'kibana';
|
||||||
private portForwardChild: ChildProcess | null = null;
|
private portForwardChild: ChildProcess | null = null;
|
||||||
private portForwardStartedByUs = false;
|
private portForwardStartedByUs = false;
|
||||||
|
private portForwardKubeconfigFile: string | null = null;
|
||||||
|
private portForwardClusterId: string | null = null;
|
||||||
private ensureInFlight: Promise<void> | null = null;
|
private ensureInFlight: Promise<void> | null = null;
|
||||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
private healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -133,6 +135,37 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isRunningInKubernetes(): boolean {
|
||||||
|
return Boolean(process.env.KUBERNETES_SERVICE_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hostnames only resolvable from inside the workload cluster (not from a laptop). */
|
||||||
|
private isClusterInternalHost(host: string): boolean {
|
||||||
|
const h = host.toLowerCase();
|
||||||
|
return (
|
||||||
|
h.includes('svc.cluster.local') ||
|
||||||
|
h.includes('.cluster.') ||
|
||||||
|
h === 'elasticsearch' ||
|
||||||
|
h === 'kibana'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private configuredElasticsearchHost(): string {
|
||||||
|
return (
|
||||||
|
this.configService.get<string>('elasticsearch.host') ||
|
||||||
|
`${this.ES_NAME}.${this.ES_NAMESPACE}.svc.cluster.local`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HTTP target: loopback when we tunnel; cluster DNS when the API pod runs in-cluster. */
|
||||||
|
private effectiveElasticsearchHost(): string {
|
||||||
|
const configured = this.configuredElasticsearchHost();
|
||||||
|
if (this.shouldAutoPortForward() && this.isClusterInternalHost(configured)) {
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
private shouldAutoPortForward(): boolean {
|
private shouldAutoPortForward(): boolean {
|
||||||
if (this.configService.get<string>('elasticsearch.autoPortForward') === 'false') {
|
if (this.configService.get<string>('elasticsearch.autoPortForward') === 'false') {
|
||||||
return false;
|
return false;
|
||||||
@@ -140,12 +173,15 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
if (process.env.ELASTICSEARCH_AUTO_PORT_FORWARD === 'false') {
|
if (process.env.ELASTICSEARCH_AUTO_PORT_FORWARD === 'false') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (this.isRunningInKubernetes()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||||
if (nodeEnv === 'production') {
|
if (nodeEnv === 'production') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const host = this.configService.get<string>('elasticsearch.host') || '';
|
const host = this.configuredElasticsearchHost();
|
||||||
return this.isLoopbackHost(host);
|
return this.isLoopbackHost(host) || this.isClusterInternalHost(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopDevPortForward(): void {
|
private stopDevPortForward(): void {
|
||||||
@@ -154,9 +190,15 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
const startedByUs = this.portForwardStartedByUs;
|
const startedByUs = this.portForwardStartedByUs;
|
||||||
const child = this.portForwardChild;
|
const child = this.portForwardChild;
|
||||||
|
const kubeconfigFile = this.portForwardKubeconfigFile;
|
||||||
this.portForwardChild = null;
|
this.portForwardChild = null;
|
||||||
this.portForwardStartedByUs = false;
|
this.portForwardStartedByUs = false;
|
||||||
|
this.portForwardKubeconfigFile = null;
|
||||||
|
this.portForwardClusterId = null;
|
||||||
child.kill('SIGTERM');
|
child.kill('SIGTERM');
|
||||||
|
if (kubeconfigFile) {
|
||||||
|
this.helmService.removeTempFile(kubeconfigFile);
|
||||||
|
}
|
||||||
if (startedByUs) {
|
if (startedByUs) {
|
||||||
this.logger.log('Stopped Elasticsearch kubectl port-forward');
|
this.logger.log('Stopped Elasticsearch kubectl port-forward');
|
||||||
}
|
}
|
||||||
@@ -236,18 +278,36 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private startDevPortForward(localPort: number): void {
|
private async startDevPortForward(localPort: number, clusterId?: string): Promise<void> {
|
||||||
if (this.portForwardChild) {
|
const targetClusterId = clusterId || null;
|
||||||
|
if (
|
||||||
|
this.portForwardChild &&
|
||||||
|
this.portForwardClusterId === targetClusterId
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.portForwardChild) {
|
||||||
|
this.stopDevPortForward();
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cluster } = await this.getK8sClients(clusterId);
|
||||||
|
const kubeconfigFile = await this.helmService.createKubeconfigFile(cluster.kubeconfig);
|
||||||
|
this.portForwardKubeconfigFile = kubeconfigFile;
|
||||||
|
this.portForwardClusterId = targetClusterId;
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
|
'--kubeconfig',
|
||||||
|
kubeconfigFile,
|
||||||
'port-forward',
|
'port-forward',
|
||||||
'-n',
|
'-n',
|
||||||
this.ES_NAMESPACE,
|
this.ES_NAMESPACE,
|
||||||
`svc/${this.ES_NAME}`,
|
`svc/${this.ES_NAME}`,
|
||||||
`${localPort}:9200`,
|
`${localPort}:9200`,
|
||||||
];
|
];
|
||||||
this.logger.log(`Starting kubectl ${args.join(' ')} (local log search)`);
|
this.logger.log(
|
||||||
|
`Starting kubectl port-forward to Elasticsearch on cluster "${cluster.name}" (local log search)`,
|
||||||
|
);
|
||||||
const child = spawn('kubectl', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
const child = spawn('kubectl', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
this.portForwardChild = child;
|
this.portForwardChild = child;
|
||||||
this.portForwardStartedByUs = true;
|
this.portForwardStartedByUs = true;
|
||||||
@@ -281,6 +341,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
*/
|
*/
|
||||||
private async ensureLocalElasticsearchAccess(options?: {
|
private async ensureLocalElasticsearchAccess(options?: {
|
||||||
waitForCluster?: boolean;
|
waitForCluster?: boolean;
|
||||||
|
clusterId?: string;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
if (this.ensureInFlight) {
|
if (this.ensureInFlight) {
|
||||||
await this.ensureInFlight;
|
await this.ensureInFlight;
|
||||||
@@ -298,6 +359,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
private async ensureLocalElasticsearchAccessImpl(options?: {
|
private async ensureLocalElasticsearchAccessImpl(options?: {
|
||||||
waitForCluster?: boolean;
|
waitForCluster?: boolean;
|
||||||
|
clusterId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (!this.shouldAutoPortForward()) {
|
if (!this.shouldAutoPortForward()) {
|
||||||
return;
|
return;
|
||||||
@@ -308,7 +370,8 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deployed = await this.isDeployed();
|
const clusterId = options?.clusterId;
|
||||||
|
let deployed = await this.isDeployed(clusterId);
|
||||||
if (!deployed && options?.waitForCluster) {
|
if (!deployed && options?.waitForCluster) {
|
||||||
this.logger.log('Waiting for logging stack after cluster reconnect…');
|
this.logger.log('Waiting for logging stack after cluster reconnect…');
|
||||||
deployed = await this.waitForLoggingStack();
|
deployed = await this.waitForLoggingStack();
|
||||||
@@ -325,7 +388,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startDevPortForward(port);
|
await this.startDevPortForward(port, clusterId);
|
||||||
const ready = await this.waitForElasticsearch(90_000);
|
const ready = await this.waitForElasticsearch(90_000);
|
||||||
if (ready) {
|
if (ready) {
|
||||||
this.reconnectAttempt = 0;
|
this.reconnectAttempt = 0;
|
||||||
@@ -341,16 +404,23 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
private localElasticsearchHint(): string {
|
private localElasticsearchHint(): string {
|
||||||
const conn = this.getConnectionInfo();
|
const conn = this.getConnectionInfo();
|
||||||
|
const configured = this.configuredElasticsearchHost();
|
||||||
if (this.isLoopbackHost(conn.host)) {
|
if (this.isLoopbackHost(conn.host)) {
|
||||||
return (
|
return (
|
||||||
`Ensure port ${conn.port} is forwarded to the cluster (the API auto-starts kubectl port-forward in development). ` +
|
`Ensure port ${conn.port} is forwarded to the cluster (the API auto-starts kubectl port-forward in development). ` +
|
||||||
`Manual: kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${conn.port}:9200`
|
`Manual: kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${conn.port}:9200`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (conn.host.includes('svc.cluster.local') || conn.host.includes('.cluster.')) {
|
if (this.isClusterInternalHost(configured)) {
|
||||||
|
if (this.isRunningInKubernetes()) {
|
||||||
return (
|
return (
|
||||||
'Run the API inside the cluster, or set ELASTICSEARCH_HOST=127.0.0.1 and keep port-forward running: ' +
|
'Ensure the logging stack is deployed on the same cluster as this API pod ' +
|
||||||
`kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${conn.port}:9200`
|
`(Helm release ${LOGGING_HELM_RELEASE} in namespace ${LOGGING_HELM_NAMESPACE}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'Run the API inside the cluster, or restart the API locally so it can auto port-forward Elasticsearch ' +
|
||||||
|
`(kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${conn.port}:9200 using the cluster kubeconfig).`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return `Ensure Elasticsearch is listening on ${conn.host}:${conn.port}.`;
|
return `Ensure Elasticsearch is listening on ${conn.host}:${conn.port}.`;
|
||||||
@@ -620,9 +690,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
*/
|
*/
|
||||||
getConnectionInfo(): { host: string; port: number; username: string; password: string } {
|
getConnectionInfo(): { host: string; port: number; username: string; password: string } {
|
||||||
return {
|
return {
|
||||||
host:
|
host: this.effectiveElasticsearchHost(),
|
||||||
this.configService.get<string>('elasticsearch.host') ||
|
|
||||||
`${this.ES_NAME}.${this.ES_NAMESPACE}.svc.cluster.local`,
|
|
||||||
port: this.configService.get<number>('elasticsearch.port') || 9200,
|
port: this.configService.get<number>('elasticsearch.port') || 9200,
|
||||||
username: 'elastic',
|
username: 'elastic',
|
||||||
password: this.ELASTIC_PASSWORD,
|
password: this.ELASTIC_PASSWORD,
|
||||||
@@ -791,6 +859,10 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldAutoPortForward()) {
|
||||||
|
await this.ensureLocalElasticsearchAccess({ clusterId });
|
||||||
|
}
|
||||||
|
|
||||||
const conn = this.getConnectionInfo();
|
const conn = this.getConnectionInfo();
|
||||||
const url = `http://${conn.host}:${conn.port}${path}`;
|
const url = `http://${conn.host}:${conn.port}${path}`;
|
||||||
const auth = Buffer.from(`${conn.username}:${conn.password}`).toString('base64');
|
const auth = Buffer.from(`${conn.username}:${conn.password}`).toString('base64');
|
||||||
@@ -800,7 +872,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
response = await this.elasticsearchFetch(url, auth, body);
|
response = await this.elasticsearchFetch(url, auth, body);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (this.shouldAutoPortForward()) {
|
if (this.shouldAutoPortForward()) {
|
||||||
await this.ensureLocalElasticsearchAccess();
|
await this.ensureLocalElasticsearchAccess({ clusterId });
|
||||||
try {
|
try {
|
||||||
response = await this.elasticsearchFetch(url, auth, body);
|
response = await this.elasticsearchFetch(url, auth, body);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -963,7 +1035,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldAutoPortForward() && !(await this.probeElasticsearch())) {
|
if (this.shouldAutoPortForward() && !(await this.probeElasticsearch())) {
|
||||||
void this.ensureLocalElasticsearchAccess();
|
void this.ensureLocalElasticsearchAccess({ clusterId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.probeElasticsearch()) {
|
if (await this.probeElasticsearch()) {
|
||||||
|
|||||||
@@ -329,6 +329,15 @@ export class HelmService {
|
|||||||
|
|
||||||
// ── Temp file helpers ───────────────────────────────────
|
// ── Temp file helpers ───────────────────────────────────
|
||||||
|
|
||||||
|
/** Write kubeconfig to a temp file for kubectl/helm CLIs (caller may delete when done). */
|
||||||
|
async createKubeconfigFile(kubeconfig: string): Promise<string> {
|
||||||
|
return this.writeTempKubeconfig(kubeconfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTempFile(filePath: string): void {
|
||||||
|
this.cleanupTempFiles(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
private async writeTempKubeconfig(kubeconfig: string): Promise<string> {
|
private async writeTempKubeconfig(kubeconfig: string): Promise<string> {
|
||||||
const tmpFile = path.join(os.tmpdir(), `cloudhost-kube-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
const tmpFile = path.join(os.tmpdir(), `cloudhost-kube-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
await fs.promises.writeFile(tmpFile, kubeconfig, { mode: 0o600 });
|
await fs.promises.writeFile(tmpFile, kubeconfig, { mode: 0o600 });
|
||||||
|
|||||||
@@ -46,12 +46,16 @@ export class LogsController {
|
|||||||
userId: string,
|
userId: string,
|
||||||
appId?: string,
|
appId?: string,
|
||||||
allowStaff = false,
|
allowStaff = false,
|
||||||
): Promise<{ applicationId?: string; applicationName?: string }> {
|
): Promise<{ applicationId?: string; applicationName?: string; clusterId?: string }> {
|
||||||
if (!appId) return {};
|
if (!appId) return {};
|
||||||
const where = allowStaff ? { id: appId } : { id: appId, userId };
|
const where = allowStaff ? { id: appId } : { id: appId, userId };
|
||||||
const app = await this.appsRepo.findOne({ where });
|
const app = await this.appsRepo.findOne({ where });
|
||||||
if (!app) throw new NotFoundException('Application not found');
|
if (!app) throw new NotFoundException('Application not found');
|
||||||
return { applicationId: app.id, applicationName: app.name };
|
return {
|
||||||
|
applicationId: app.id,
|
||||||
|
applicationName: app.name,
|
||||||
|
clusterId: app.clusterId || undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private isStaff(role: string): boolean {
|
private isStaff(role: string): boolean {
|
||||||
@@ -60,8 +64,15 @@ export class LogsController {
|
|||||||
|
|
||||||
@Get('status')
|
@Get('status')
|
||||||
@ApiOperation({ summary: 'Check if central logging is available' })
|
@ApiOperation({ summary: 'Check if central logging is available' })
|
||||||
async getStatus() {
|
@ApiQuery({ name: 'appId', required: false, description: 'Resolve logging cluster from this app' })
|
||||||
return this.esService.getLoggingStatus();
|
async getStatus(
|
||||||
|
@Request() req: AuthenticatedRequest,
|
||||||
|
@Query('appId') appId?: string,
|
||||||
|
) {
|
||||||
|
const appFilters = appId
|
||||||
|
? await this.resolveAppFilters(req.user.id, appId, this.isStaff(req.user.role))
|
||||||
|
: {};
|
||||||
|
return this.esService.getLoggingStatus(appFilters.clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -102,7 +113,9 @@ export class LogsController {
|
|||||||
|
|
||||||
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
||||||
|
|
||||||
return this.esService.searchLogs(userId, {
|
return this.esService.searchLogs(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
...appFilters,
|
...appFilters,
|
||||||
workload,
|
workload,
|
||||||
level: level?.toLowerCase(),
|
level: level?.toLowerCase(),
|
||||||
@@ -111,7 +124,9 @@ export class LogsController {
|
|||||||
search,
|
search,
|
||||||
page: parseInt(page || '1', 10),
|
page: parseInt(page || '1', 10),
|
||||||
limit: Math.min(parseInt(limit || '100', 10), 1000),
|
limit: Math.min(parseInt(limit || '100', 10), 1000),
|
||||||
});
|
},
|
||||||
|
appFilters.clusterId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('stream')
|
@Get('stream')
|
||||||
@@ -127,13 +142,17 @@ export class LogsController {
|
|||||||
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
||||||
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
return this.esService.searchLogs(userId, {
|
return this.esService.searchLogs(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
...appFilters,
|
...appFilters,
|
||||||
workload,
|
workload,
|
||||||
from: fiveMinAgo,
|
from: fiveMinAgo,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
page: 1,
|
page: 1,
|
||||||
});
|
},
|
||||||
|
appFilters.clusterId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('stats')
|
@Get('stats')
|
||||||
@@ -149,11 +168,15 @@ export class LogsController {
|
|||||||
) {
|
) {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
||||||
return this.esService.searchLogStats(userId, {
|
return this.esService.searchLogStats(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
...appFilters,
|
...appFilters,
|
||||||
workload,
|
workload,
|
||||||
period: period || '24h',
|
period: period || '24h',
|
||||||
});
|
},
|
||||||
|
appFilters.clusterId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('errors')
|
@Get('errors')
|
||||||
@@ -171,12 +194,16 @@ export class LogsController {
|
|||||||
) {
|
) {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
const appFilters = await this.resolveAppFilters(userId, appId, this.isStaff(req.user.role));
|
||||||
const hits = await this.esService.searchRecentErrors(userId, {
|
const hits = await this.esService.searchRecentErrors(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
...appFilters,
|
...appFilters,
|
||||||
workload,
|
workload,
|
||||||
hours: parseInt(hours || '24', 10),
|
hours: parseInt(hours || '24', 10),
|
||||||
limit: Math.min(parseInt(limit || '50', 10), 500),
|
limit: Math.min(parseInt(limit || '50', 10), 500),
|
||||||
});
|
},
|
||||||
|
appFilters.clusterId,
|
||||||
|
);
|
||||||
return { hits, total: hits.length };
|
return { hits, total: hits.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ function LogsPageContent() {
|
|||||||
}, [initialAppId]);
|
}, [initialAppId]);
|
||||||
|
|
||||||
const { data: loggingStatus, isFetching: statusFetching } = useQuery({
|
const { data: loggingStatus, isFetching: statusFetching } = useQuery({
|
||||||
queryKey: ['logs-status'],
|
queryKey: ['logs-status', appId],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
api
|
api
|
||||||
.get('/logs/status')
|
.get('/logs/status', { params: appId ? { appId } : undefined })
|
||||||
.then((r) =>
|
.then((r) =>
|
||||||
r.data as {
|
r.data as {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
@@ -185,7 +185,7 @@ function LogsPageContent() {
|
|||||||
const totalPages = logsResult ? Math.max(1, Math.ceil(logsResult.total / logsResult.limit)) : 1;
|
const totalPages = logsResult ? Math.max(1, Math.ceil(logsResult.total / logsResult.limit)) : 1;
|
||||||
|
|
||||||
if (loggingStatus && !loggingStatus.available) {
|
if (loggingStatus && !loggingStatus.available) {
|
||||||
const isRecovering = loggingStatus.recovering || loggingStatus.deployed === true;
|
const isRecovering = loggingStatus.recovering === true;
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto card p-8 text-center">
|
<div className="max-w-3xl mx-auto card p-8 text-center">
|
||||||
{isRecovering ? (
|
{isRecovering ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user