/**
* Log Discovery Tools
* Enhanced tools for discovering and accessing log containers across all environments
* Especially important for Production where container names may vary
*/
import ResponseBuilder from '../response-builder';
import OutputLogger from '../output-logger';
import ProjectTools from './project-tools';
import StorageTools from './storage-tools';
/**
* Log container pattern definition
*/
interface LogContainerPattern {
pattern: RegExp;
type: string;
description: string;
}
/**
* Container match result
*/
interface ContainerMatch {
type: string;
description: string;
}
/**
* Log container info
*/
interface LogContainerInfo {
name: string;
type: string;
description: string;
}
/**
* Environment discovery result
*/
interface EnvironmentDiscoveryResult {
accessible: boolean;
totalContainers?: number;
logContainers?: LogContainerInfo[];
otherContainers?: string[];
error?: string;
}
/**
* Discovery results (all environments)
*/
interface DiscoveryResults {
[environment: string]: EnvironmentDiscoveryResult;
}
/**
* Discovery arguments
*/
interface DiscoveryArgs {
[key: string]: any;
}
/**
* Response with structured content
*/
interface StructuredResponse {
structuredContent?: {
data?: {
containers?: string[];
};
};
data?: {
containers?: string[];
};
error?: string;
content?: Array<{ text?: string }>;
result?: {
content?: Array<{ text?: string }>;
};
}
class LogDiscoveryTools {
// Known log container patterns
static LOG_CONTAINER_PATTERNS: LogContainerPattern[] = [
// Standard DXP containers
{ pattern: /^appservicelogs$/, type: 'application', description: 'Application logs' },
{ pattern: /^webservicelogs$/, type: 'web', description: 'Web server logs' },
{ pattern: /^cloudflarelogpush$/, type: 'cloudflare', description: 'Cloudflare logs (beta)' },
// App Service Insights containers
{ pattern: /^insights-logs-appserviceconsolelogs$/, type: 'application', description: 'App Service console logs' },
{ pattern: /^insights-logs-appservicehttplogs$/, type: 'web', description: 'App Service HTTP logs' },
{ pattern: /^insights-logs-/, type: 'insights', description: 'Application Insights logs' },
// Alternative Azure containers (commonly found in Production)
{ pattern: /^azure-application-logs$/, type: 'application', description: 'Azure application logs' },
{ pattern: /^azure-web-logs$/, type: 'web', description: 'Azure web server logs' },
{ pattern: /^azure-logs-/, type: 'azure', description: 'Azure logs' },
// Application Insights containers
{ pattern: /applicationinsights/, type: 'appinsights', description: 'Application Insights data' },
{ pattern: /appinsights/, type: 'appinsights', description: 'Application Insights data' },
// Enhanced generic log patterns
{ pattern: /logs?$/, type: 'generic', description: 'Generic log container' },
{ pattern: /log-/, type: 'generic', description: 'Generic log container' },
{ pattern: /-logs?$/, type: 'generic', description: 'Log container (suffix)' },
{ pattern: /diagnostic.*logs?/i, type: 'diagnostic', description: 'Diagnostic logs' },
{ pattern: /inte-.*logs?/i, type: 'integration', description: 'Integration environment logs' },
{ pattern: /backup.*logs?/i, type: 'backup', description: 'Backup logs' },
{ pattern: /^db-backups?-/i, type: 'database', description: 'Database backup container' }
];
/**
* Discover all log containers across environments
*/
static async discoverLogContainers(args: DiscoveryArgs): Promise<any> {
try {
OutputLogger.info('π Discovering log containers across all environments...\n');
// Resolve project configuration
const resolved = ProjectTools.resolveCredentials(args);
if (!resolved.success || !resolved.credentials) {
return ResponseBuilder.invalidParams('Missing required project configuration');
}
const projectConfig = resolved.credentials;
const projectName = resolved.project ? resolved.project.name : 'Unknown';
OutputLogger.info(`π Project: ${projectName}`);
OutputLogger.info(`π Using API key: ${projectConfig.apiKey?.substring(0, 8)}...`);
// Test each environment
const environments = ['Production', 'Preproduction', 'Integration'];
const discoveryResults: DiscoveryResults = {};
for (const env of environments) {
OutputLogger.info(`\nπ Checking ${env} environment...`);
try {
const envArgs = {
...projectConfig,
environment: env
};
if (process.env.DEBUG === 'true') {
console.error(`[DISCOVER_LOGS] Calling handleListStorageContainers for ${env}`);
console.error(`[DISCOVER_LOGS] Args:`, {
hasApiKey: !!envArgs.apiKey,
hasApiSecret: !!envArgs.apiSecret,
hasProjectId: !!envArgs.projectId,
environment: envArgs.environment
});
}
// Get all containers for this environment
const containersResult = await StorageTools.handleListStorageContainers(envArgs as any);
if (process.env.DEBUG === 'true') {
console.error(`[DISCOVER_LOGS] Result type:`, typeof containersResult);
console.error(`[DISCOVER_LOGS] Result structure:`, Object.keys(containersResult || {}));
}
const containers = this.extractContainerList(containersResult);
if (process.env.DEBUG === 'true') {
console.error(`[DISCOVER_LOGS] Extracted ${containers.length} containers from result`);
}
if (!containers || containers.length === 0) {
OutputLogger.info(` β οΈ No containers accessible in ${env}`);
discoveryResults[env] = {
accessible: false,
logContainers: [],
otherContainers: []
};
continue;
}
// Identify log containers
const logContainers: LogContainerInfo[] = [];
const otherContainers: string[] = [];
for (const container of containers) {
const match = this.identifyLogContainer(container);
if (match) {
logContainers.push({
name: container,
type: match.type,
description: match.description
});
} else {
otherContainers.push(container);
}
}
discoveryResults[env] = {
accessible: true,
totalContainers: containers.length,
logContainers: logContainers,
otherContainers: otherContainers
};
// Display results for this environment
OutputLogger.info(` β
Found ${containers.length} total containers`);
if (logContainers.length > 0) {
OutputLogger.info(` π Log containers (${logContainers.length}):`);
for (const log of logContainers) {
OutputLogger.info(` β’ ${log.name} (${log.description})`);
}
} else {
OutputLogger.info(` β οΈ No log containers found!`);
}
if (otherContainers.length > 0) {
OutputLogger.info(` π¦ ALL other containers (${otherContainers.length}):`);
for (const container of otherContainers) {
OutputLogger.info(` β’ ${container}`);
}
}
} catch (error: any) {
if (process.env.DEBUG === 'true') {
console.error(`[DISCOVER_LOGS] Full error for ${env}:`, error);
console.error(`[DISCOVER_LOGS] Error stack:`, error.stack);
}
OutputLogger.error(` β Error accessing ${env}: ${error.message}`);
// Provide more context in the error
let errorDetail = error.message;
if (error.response) {
errorDetail += ` (HTTP ${error.response.statusCode || error.response.status})`;
}
if (error.code) {
errorDetail += ` [${error.code}]`;
}
discoveryResults[env] = {
accessible: false,
error: errorDetail
};
}
}
// Generate diagnostic report
return this.generateDiagnosticReport(discoveryResults, projectName);
} catch (error: any) {
return ResponseBuilder.error(`Log discovery failed: ${error.message}`);
}
}
/**
* Identify if a container is a log container
*/
static identifyLogContainer(containerName: string): ContainerMatch | null {
for (const pattern of this.LOG_CONTAINER_PATTERNS) {
if (pattern.pattern.test(containerName)) {
return {
type: pattern.type,
description: pattern.description
};
}
}
return null;
}
/**
* Extract container list from response
*/
static extractContainerList(response: any): string[] {
if (!response) {
return [];
}
// DXP-86: First check for structured data (preferred method)
if (typeof response === 'object' && response !== null) {
const structuredResponse = response as StructuredResponse;
// Check for structuredContent.data.containers (MCP protocol format)
if (structuredResponse.structuredContent?.data?.containers) {
if (process.env.DEBUG === 'true') {
console.error('[DISCOVER_LOGS] Found containers in structuredContent.data.containers');
}
return structuredResponse.structuredContent.data.containers;
}
// Check for direct data.containers format
if (structuredResponse.data?.containers) {
if (process.env.DEBUG === 'true') {
console.error('[DISCOVER_LOGS] Found containers in data.containers');
}
return structuredResponse.data.containers;
}
// Check for error response
if (structuredResponse.error) {
OutputLogger.info(`Error in container list response: ${structuredResponse.error}`);
return [];
}
}
// Fall back to text parsing for legacy formats
let text = '';
if (typeof response === 'object' && response !== null) {
const structuredResponse = response as StructuredResponse;
if (structuredResponse.content && Array.isArray(structuredResponse.content) && structuredResponse.content[0]) {
text = structuredResponse.content[0].text || '';
} else if (structuredResponse.result && structuredResponse.result.content && Array.isArray(structuredResponse.result.content)) {
const content = structuredResponse.result.content[0];
if (content && content.text) {
text = content.text;
}
} else {
text = JSON.stringify(response);
}
} else if (typeof response === 'string') {
text = response;
}
if (!text) {
return [];
}
if (process.env.DEBUG === 'true') {
console.error('[DISCOVER_LOGS] Falling back to text parsing');
}
const containers: string[] = [];
const lines = text.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
// Look for numbered emoji format: "1. π¦ container-name"
let match = line.match(/^\d+\.\s*π¦\s*(.+)$/);
if (match) {
containers.push(match[1].trim());
continue;
}
// Look for simple bullet format: "- container-name" or "β’ container-name"
match = line.match(/^[\s\-β’]\s*([^\s\-β’].+)$/);
if (match && !match[1].includes('Storage Container') && !match[1].includes('---')) {
const name = match[1].trim();
if (name && !name.includes('|') && !name.includes('Environment')) {
containers.push(name);
}
continue;
}
// Look for markdown table format: "| container-name |"
match = line.match(/\|\s*([^|]+)\s*\|/);
if (match && !match[1].includes('Storage Container') && !match[1].includes('---')) {
containers.push(match[1].trim());
continue;
}
// Try JSON parsing if it looks like container data
if (line.includes('{') || line.includes('[')) {
try {
const data = JSON.parse(line);
if (Array.isArray(data)) {
data.forEach((item: any) => {
const name = item.StorageContainer || item.Name || item.ContainerName;
if (name) containers.push(name);
});
} else if (data.StorageContainer || data.Name || data.ContainerName) {
containers.push(data.StorageContainer || data.Name || data.ContainerName);
}
} catch (e) {
// Not JSON, continue
}
}
}
return containers.filter(Boolean);
}
/**
* Generate diagnostic report
*/
static generateDiagnosticReport(results: DiscoveryResults, projectName: string): any {
let message = `# π Log Container Discovery Report\n\n`;
message += `**Project**: ${projectName}\n`;
message += `**Timestamp**: ${new Date().toISOString()}\n\n`;
// Check for critical issues
const prodResult = results['Production'];
const hasProductionIssue = !prodResult?.accessible || prodResult?.logContainers?.length === 0;
if (hasProductionIssue) {
message += `## π¨ CRITICAL ISSUE DETECTED\n\n`;
if (!prodResult?.accessible) {
message += `β **Cannot access Production containers**\n`;
message += ` Error: ${prodResult?.error || 'Access denied'}\n\n`;
message += `### Recommended Actions:\n`;
message += `1. Verify API key has Production access\n`;
message += `2. Contact Optimizely Support to enable Production logging\n`;
message += `3. Check if Production environment exists for this project\n\n`;
} else if (prodResult?.logContainers?.length === 0) {
message += `β οΈ **No log containers found in Production**\n`;
message += ` Found ${prodResult.totalContainers} containers, but none are log containers\n\n`;
message += `### Possible Causes:\n`;
message += `1. **Logging not enabled**: Contact Optimizely Support to enable Production logging\n`;
message += `2. **Non-standard names**: Containers may have custom names\n`;
message += `3. **Permission issue**: API key may lack log container access\n\n`;
if (prodResult.otherContainers?.length && prodResult.otherContainers.length > 0) {
message += `### ALL containers found in Production (${prodResult.otherContainers.length} total):\n`;
for (const container of prodResult.otherContainers) {
message += ` β’ \`${container}\`\n`;
}
message += `\n`;
message += `π‘ **Tip**: Any of these might contain logs with non-standard names.\n`;
message += `Try: \`download_logs environment: "Production" containerName: "[container-name]"\`\n\n`;
}
}
}
// Environment summary
message += `## π Environment Summary\n\n`;
for (const [env, result] of Object.entries(results)) {
const icon = result.accessible ? 'β
' : 'β';
const logCount = result.logContainers?.length || 0;
message += `### ${icon} ${env}\n`;
if (!result.accessible) {
message += ` Status: **Not accessible**\n`;
if (result.error) {
message += ` Error: ${result.error}\n`;
}
} else {
message += ` Status: **Accessible**\n`;
message += ` Total containers: ${result.totalContainers}\n`;
message += ` Log containers: ${logCount}\n`;
if (logCount > 0 && result.logContainers) {
message += ` Available logs:\n`;
for (const log of result.logContainers) {
message += ` β’ \`${log.name}\` - ${log.description}\n`;
}
}
}
message += `\n`;
}
// Recommendations
message += `## π‘ Recommendations\n\n`;
if (hasProductionIssue) {
message += `### For Production Log Access:\n`;
message += `1. **Contact Optimizely Support** with this information:\n`;
message += ` - Request to enable Production logging\n`;
message += ` - Project ID: Include your project ID\n`;
message += ` - Mention you need access to Application Insights logs\n`;
message += ` - Request both console and HTTP logs\n\n`;
message += `2. **Alternative Methods** while waiting:\n`;
message += ` - Check DXP Management Portal for downloadable logs\n`;
message += ` - Use Application Insights in Azure Portal (if accessible)\n`;
message += ` - Request Kudu access from Optimizely Support\n\n`;
}
// Working examples from other environments
const workingEnvs = Object.entries(results)
.filter(([_env, r]) => r.accessible && r.logContainers && r.logContainers.length > 0)
.map(([env, r]) => ({ env, containers: r.logContainers! }));
if (workingEnvs.length > 0) {
message += `### β
Working Log Access:\n`;
for (const { env, containers } of workingEnvs) {
message += `\n**${env}** - Use these commands:\n`;
for (const container of containers) {
message += `\`\`\`\n`;
message += `download_logs environment: "${env}" containerName: "${container.name}"\n`;
message += `\`\`\`\n`;
}
}
}
message += `\n## π Support Contact\n`;
message += `If Production logs remain inaccessible:\n`;
message += `β’ **Optimizely Support**: support@optimizely.com\n`;
message += `β’ **Reference**: "Production Application Insights log access"\n`;
message += `β’ **Include**: This diagnostic report\n`;
return ResponseBuilder.success(message);
}
}
export default LogDiscoveryTools;