plex-mcp 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +16 -0
- package/Dockerfile +18 -4
- package/README.md +42 -1
- package/TODO.md +4 -0
- package/eslint.config.js +189 -0
- package/http-logger.js +387 -0
- package/index.js +2819 -1375
- package/llm-utils.js +393 -0
- package/package.json +7 -1
- package/smithery.yaml +14 -5
package/.dockerignore
ADDED
package/Dockerfile
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
|
|
2
|
-
#
|
|
2
|
+
# Dockerfile for Plex MCP Server with SSE support
|
|
3
3
|
FROM node:lts-alpine AS base
|
|
4
4
|
WORKDIR /app
|
|
5
5
|
|
|
6
|
-
# Install dependencies
|
|
6
|
+
# Install system dependencies and npm packages
|
|
7
|
+
RUN apk add --no-cache wget
|
|
7
8
|
COPY package.json package-lock.json ./
|
|
8
9
|
RUN npm ci --ignore-scripts
|
|
9
10
|
|
|
10
11
|
# Copy source
|
|
11
12
|
COPY . .
|
|
12
13
|
|
|
13
|
-
#
|
|
14
|
+
# Create non-root user
|
|
15
|
+
RUN addgroup -g 1001 -S nodejs
|
|
16
|
+
RUN adduser -S nextjs -u 1001
|
|
17
|
+
|
|
18
|
+
# Environment variables
|
|
14
19
|
ENV PLEX_VERIFY_SSL=true
|
|
20
|
+
ENV MCP_TRANSPORT=sse
|
|
21
|
+
ENV PORT=3000
|
|
22
|
+
|
|
23
|
+
# Expose the SSE port
|
|
24
|
+
EXPOSE 3000
|
|
25
|
+
|
|
26
|
+
# Health check for SSE mode
|
|
27
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
28
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
15
29
|
|
|
16
30
|
# Start the MCP server
|
|
17
|
-
USER
|
|
31
|
+
USER nextjs
|
|
18
32
|
CMD ["node", "index.js"]
|
package/README.md
CHANGED
|
@@ -40,6 +40,34 @@ Add to your Claude Desktop MCP settings:
|
|
|
40
40
|
|
|
41
41
|
**Note:** Replace `your-plex-server:32400` with your actual Plex server address and port.
|
|
42
42
|
|
|
43
|
+
## HTTP Debug Logging
|
|
44
|
+
|
|
45
|
+
For troubleshooting connection issues, you can enable comprehensive HTTP request/response logging:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"plex": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["plex-mcp"],
|
|
53
|
+
"env": {
|
|
54
|
+
"PLEX_URL": "http://your-plex-server:32400",
|
|
55
|
+
"MCP_HTTP_DEBUG": "true"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When `MCP_HTTP_DEBUG=true`, the server will log:
|
|
63
|
+
- **Request Details**: Method, URL, headers, parameters, timing
|
|
64
|
+
- **Response Details**: Status codes, headers, response size, duration
|
|
65
|
+
- **Error Diagnostics**: Connection details, TLS/SSL certificate info, troubleshooting suggestions
|
|
66
|
+
- **Performance Metrics**: DNS lookup, TCP connection, TLS handshake timing
|
|
67
|
+
- **Security**: All sensitive headers (tokens, auth) are automatically redacted
|
|
68
|
+
|
|
69
|
+
All logs are structured JSON with correlation IDs for request/response tracking.
|
|
70
|
+
|
|
43
71
|
## What You Can Do
|
|
44
72
|
|
|
45
73
|
**Search & Browse**
|
|
@@ -63,9 +91,22 @@ Add to your Claude Desktop MCP settings:
|
|
|
63
91
|
- Check watch status and progress
|
|
64
92
|
- View library statistics and listening stats
|
|
65
93
|
|
|
94
|
+
**Resources (Data Access)**
|
|
95
|
+
- Access structured data via URI: `plex://libraries`, `plex://recent`, `plex://playlists`
|
|
96
|
+
- Library-specific data: `plex://library/{id}`, `plex://library/{id}/recent`
|
|
97
|
+
- Server statistics and connection status
|
|
98
|
+
- JSON-formatted data for analysis and automation
|
|
99
|
+
|
|
100
|
+
**Prompts (AI Assistance)**
|
|
101
|
+
- **playlist_description**: Generate creative playlist descriptions
|
|
102
|
+
- **content_recommendation**: Get personalized content recommendations
|
|
103
|
+
- **smart_playlist_rules**: Create smart playlist criteria and rules
|
|
104
|
+
- **media_analysis**: Analyze library content and patterns
|
|
105
|
+
- **server_troubleshooting**: Diagnose server connection issues
|
|
106
|
+
|
|
66
107
|
## Status
|
|
67
108
|
|
|
68
|
-
**✅ Working:** Search, browse, playlists, media info, library stats, watch history, collections, music discovery
|
|
109
|
+
**✅ Working:** Search, browse, playlists, media info, library stats, watch history, collections, music discovery, resources, prompts
|
|
69
110
|
|
|
70
111
|
**❌ Disabled:** Smart playlists (filter logic broken)
|
|
71
112
|
|
package/TODO.md
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
## Unimplemented Plex API Features
|
|
24
24
|
|
|
25
|
+
# Random human shit
|
|
26
|
+
|
|
27
|
+
SMithery integration shows tools but no "provided resources" or "provided prompts" - we should identify what these mean and whether they are useful in the context of the Plex MCP.
|
|
28
|
+
|
|
25
29
|
### Critical Missing APIs (High Priority)
|
|
26
30
|
|
|
27
31
|
#### Session Management
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const js = require('@eslint/js');
|
|
2
|
+
|
|
3
|
+
module.exports = [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.js'],
|
|
7
|
+
ignores: [
|
|
8
|
+
'node_modules/',
|
|
9
|
+
'coverage/',
|
|
10
|
+
'*.min.js',
|
|
11
|
+
'dist/',
|
|
12
|
+
'build/',
|
|
13
|
+
'.git/'
|
|
14
|
+
],
|
|
15
|
+
languageOptions: {
|
|
16
|
+
ecmaVersion: 2022,
|
|
17
|
+
sourceType: 'commonjs',
|
|
18
|
+
globals: {
|
|
19
|
+
console: 'readonly',
|
|
20
|
+
process: 'readonly',
|
|
21
|
+
Buffer: 'readonly',
|
|
22
|
+
__dirname: 'readonly',
|
|
23
|
+
__filename: 'readonly',
|
|
24
|
+
module: 'readonly',
|
|
25
|
+
require: 'readonly',
|
|
26
|
+
exports: 'readonly',
|
|
27
|
+
global: 'readonly',
|
|
28
|
+
setTimeout: 'readonly',
|
|
29
|
+
setInterval: 'readonly',
|
|
30
|
+
clearTimeout: 'readonly',
|
|
31
|
+
clearInterval: 'readonly',
|
|
32
|
+
URLSearchParams: 'readonly'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
// Error prevention
|
|
37
|
+
'no-console': 'off', // We need console for this CLI tool
|
|
38
|
+
'no-unused-vars': ['warn', { // Changed to warn instead of error
|
|
39
|
+
argsIgnorePattern: '^_',
|
|
40
|
+
varsIgnorePattern: '^_',
|
|
41
|
+
caughtErrorsIgnorePattern: '^_'
|
|
42
|
+
}],
|
|
43
|
+
'no-undef': 'error',
|
|
44
|
+
'no-unreachable': 'error',
|
|
45
|
+
'no-constant-condition': 'error',
|
|
46
|
+
'no-dupe-keys': 'error',
|
|
47
|
+
'no-duplicate-case': 'error',
|
|
48
|
+
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
49
|
+
'no-ex-assign': 'error',
|
|
50
|
+
'no-extra-boolean-cast': 'error',
|
|
51
|
+
'no-func-assign': 'error',
|
|
52
|
+
'no-inner-declarations': 'error',
|
|
53
|
+
'no-invalid-regexp': 'error',
|
|
54
|
+
'no-irregular-whitespace': 'error',
|
|
55
|
+
'no-obj-calls': 'error',
|
|
56
|
+
'no-regex-spaces': 'error',
|
|
57
|
+
'no-sparse-arrays': 'error',
|
|
58
|
+
'no-unexpected-multiline': 'error',
|
|
59
|
+
'use-isnan': 'error',
|
|
60
|
+
'valid-typeof': 'error',
|
|
61
|
+
|
|
62
|
+
// Best practices
|
|
63
|
+
curly: ['error', 'all'],
|
|
64
|
+
'dot-notation': 'error',
|
|
65
|
+
eqeqeq: ['error', 'always'],
|
|
66
|
+
'no-alert': 'error',
|
|
67
|
+
'no-caller': 'error',
|
|
68
|
+
'no-eval': 'error',
|
|
69
|
+
'no-extend-native': 'error',
|
|
70
|
+
'no-extra-bind': 'error',
|
|
71
|
+
'no-fallthrough': 'error',
|
|
72
|
+
'no-case-declarations': 'off', // Allow declarations in case blocks
|
|
73
|
+
'no-floating-decimal': 'error',
|
|
74
|
+
'no-implied-eval': 'error',
|
|
75
|
+
'no-lone-blocks': 'error',
|
|
76
|
+
'no-loop-func': 'error',
|
|
77
|
+
'no-multi-spaces': 'error',
|
|
78
|
+
'no-new': 'error',
|
|
79
|
+
'no-new-func': 'error',
|
|
80
|
+
'no-new-wrappers': 'error',
|
|
81
|
+
'no-octal': 'error',
|
|
82
|
+
'no-octal-escape': 'error',
|
|
83
|
+
'no-redeclare': 'error',
|
|
84
|
+
'no-return-assign': 'error',
|
|
85
|
+
'no-script-url': 'error',
|
|
86
|
+
'no-self-assign': 'error',
|
|
87
|
+
'no-self-compare': 'error',
|
|
88
|
+
'no-sequences': 'error',
|
|
89
|
+
'no-throw-literal': 'error',
|
|
90
|
+
'no-unmodified-loop-condition': 'error',
|
|
91
|
+
'no-unused-expressions': 'error',
|
|
92
|
+
'no-useless-call': 'error',
|
|
93
|
+
'no-useless-concat': 'error',
|
|
94
|
+
'no-void': 'error',
|
|
95
|
+
radix: 'error',
|
|
96
|
+
'wrap-iife': ['error', 'any'],
|
|
97
|
+
yoda: 'error',
|
|
98
|
+
|
|
99
|
+
// Variables
|
|
100
|
+
'no-delete-var': 'error',
|
|
101
|
+
'no-label-var': 'error',
|
|
102
|
+
'no-restricted-globals': 'error',
|
|
103
|
+
'no-shadow': 'error',
|
|
104
|
+
'no-shadow-restricted-names': 'error',
|
|
105
|
+
'no-undef-init': 'error',
|
|
106
|
+
'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
|
|
107
|
+
|
|
108
|
+
// Style
|
|
109
|
+
'array-bracket-spacing': ['error', 'never'],
|
|
110
|
+
'block-spacing': 'error',
|
|
111
|
+
'brace-style': ['warn', '1tbs', { allowSingleLine: true }], // Changed to warn
|
|
112
|
+
camelcase: ['error', { properties: 'never', allow: [
|
|
113
|
+
'play_count_min', 'play_count_max', 'last_played_after', 'last_played_before',
|
|
114
|
+
'played_in_last_days', 'never_played', 'content_rating', 'audio_format',
|
|
115
|
+
'bpm_min', 'bmp_max', 'musical_key', 'dynamic_range_min', 'dynamic_range_max',
|
|
116
|
+
'loudness_min', 'loudness_max', 'acoustic_ratio_min', 'acoustic_ratio_max',
|
|
117
|
+
'file_size_min', 'file_size_max', 'year_min', 'year_max', 'rating_min',
|
|
118
|
+
'rating_max', 'duration_min', 'duration_max', 'added_after', 'added_before',
|
|
119
|
+
'library_id', 'chunk_size', 'chunk_offset', 'time_period', 'music_library_id',
|
|
120
|
+
'include_recommendations', 'include_details', 'account_id', 'playlist_type',
|
|
121
|
+
'playlist_id', 'item_key', 'item_keys', 'collection_id', 'pin_id'
|
|
122
|
+
] }],
|
|
123
|
+
'comma-dangle': ['error', 'never'],
|
|
124
|
+
'comma-spacing': ['error', { before: false, after: true }],
|
|
125
|
+
'comma-style': ['error', 'last'],
|
|
126
|
+
'computed-property-spacing': ['error', 'never'],
|
|
127
|
+
'eol-last': 'error',
|
|
128
|
+
'func-call-spacing': ['error', 'never'],
|
|
129
|
+
indent: ['error', 2, { SwitchCase: 1 }],
|
|
130
|
+
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
|
|
131
|
+
'keyword-spacing': ['error', { before: true, after: true }],
|
|
132
|
+
'linebreak-style': ['error', 'unix'],
|
|
133
|
+
'max-len': ['error', {
|
|
134
|
+
code: 150, // Increased for this complex codebase
|
|
135
|
+
tabWidth: 2,
|
|
136
|
+
ignoreUrls: true,
|
|
137
|
+
ignoreStrings: true,
|
|
138
|
+
ignoreTemplateLiterals: true,
|
|
139
|
+
ignoreRegExpLiterals: true,
|
|
140
|
+
ignoreComments: true
|
|
141
|
+
}],
|
|
142
|
+
'new-cap': ['error', { newIsCap: true, capIsNew: false }],
|
|
143
|
+
'new-parens': 'error',
|
|
144
|
+
'no-array-constructor': 'error',
|
|
145
|
+
'no-mixed-spaces-and-tabs': 'error',
|
|
146
|
+
'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }],
|
|
147
|
+
'no-new-object': 'error',
|
|
148
|
+
'no-spaced-func': 'error',
|
|
149
|
+
'no-trailing-spaces': 'error',
|
|
150
|
+
'no-unneeded-ternary': 'error',
|
|
151
|
+
'object-curly-spacing': ['error', 'always'],
|
|
152
|
+
'one-var': ['error', 'never'],
|
|
153
|
+
'operator-assignment': ['error', 'always'],
|
|
154
|
+
'operator-linebreak': ['error', 'after'],
|
|
155
|
+
'padded-blocks': ['error', 'never'],
|
|
156
|
+
'quote-props': ['error', 'as-needed'],
|
|
157
|
+
quotes: ['error', 'single', { avoidEscape: true }],
|
|
158
|
+
semi: ['error', 'always'],
|
|
159
|
+
'semi-spacing': ['error', { before: false, after: true }],
|
|
160
|
+
'space-before-blocks': 'error',
|
|
161
|
+
'space-before-function-paren': ['error', 'never'],
|
|
162
|
+
'space-in-parens': ['error', 'never'],
|
|
163
|
+
'space-infix-ops': 'error',
|
|
164
|
+
'space-unary-ops': ['error', { words: true, nonwords: false }],
|
|
165
|
+
'spaced-comment': ['error', 'always']
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
files: ['tests/**/*.js', '**/*.test.js'],
|
|
170
|
+
languageOptions: {
|
|
171
|
+
globals: {
|
|
172
|
+
describe: 'readonly',
|
|
173
|
+
it: 'readonly',
|
|
174
|
+
test: 'readonly',
|
|
175
|
+
expect: 'readonly',
|
|
176
|
+
beforeEach: 'readonly',
|
|
177
|
+
afterEach: 'readonly',
|
|
178
|
+
beforeAll: 'readonly',
|
|
179
|
+
afterAll: 'readonly',
|
|
180
|
+
jest: 'readonly'
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
rules: {
|
|
184
|
+
'no-unused-expressions': 'off', // Allow expect().toBe() etc
|
|
185
|
+
'max-len': ['error', { code: 999 }] // Longer lines allowed in tests
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
|
package/http-logger.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Logger - A reusable HTTP logging utility for MCP servers
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Request/response logging with correlation IDs
|
|
6
|
+
* - Comprehensive diagnostic information
|
|
7
|
+
* - Configurable debug levels
|
|
8
|
+
* - Header sanitization for security
|
|
9
|
+
* - Connection and TLS error details
|
|
10
|
+
* - Performance timing
|
|
11
|
+
* - Structured JSON output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class HttpLogger {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.debugLogging = options.debug ?? (process.env.MCP_HTTP_DEBUG === 'true');
|
|
17
|
+
this.logLevel = options.logLevel || 'debug';
|
|
18
|
+
this.requestCounter = 0;
|
|
19
|
+
this.serviceName = options.serviceName || 'mcp-server';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate unique correlation ID for request tracking
|
|
24
|
+
*/
|
|
25
|
+
generateCorrelationId() {
|
|
26
|
+
return `req_${Date.now()}_${++this.requestCounter}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Log HTTP request details
|
|
31
|
+
*/
|
|
32
|
+
logRequest(config, correlationId) {
|
|
33
|
+
if (!this.debugLogging) { return; }
|
|
34
|
+
|
|
35
|
+
const logData = {
|
|
36
|
+
type: 'HTTP_REQUEST',
|
|
37
|
+
service: this.serviceName,
|
|
38
|
+
correlationId,
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
request: {
|
|
41
|
+
method: config.method?.toUpperCase() || 'GET',
|
|
42
|
+
url: config.url,
|
|
43
|
+
baseURL: config.baseURL,
|
|
44
|
+
fullUrl: this.buildFullUrl(config),
|
|
45
|
+
headers: this.sanitizeHeaders(config.headers || {}),
|
|
46
|
+
params: config.params,
|
|
47
|
+
timeout: config.timeout,
|
|
48
|
+
httpsAgent: config.httpsAgent ? 'configured' : 'default',
|
|
49
|
+
maxRedirects: config.maxRedirects,
|
|
50
|
+
validateStatus: typeof config.validateStatus === 'function' ? 'custom' : 'default'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.log('debug', JSON.stringify(logData, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log HTTP response details
|
|
59
|
+
*/
|
|
60
|
+
logResponse(response, correlationId, startTime) {
|
|
61
|
+
if (!this.debugLogging) { return; }
|
|
62
|
+
|
|
63
|
+
const duration = Date.now() - startTime;
|
|
64
|
+
const logData = {
|
|
65
|
+
type: 'HTTP_RESPONSE',
|
|
66
|
+
service: this.serviceName,
|
|
67
|
+
correlationId,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
response: {
|
|
70
|
+
status: response.status,
|
|
71
|
+
statusText: response.statusText,
|
|
72
|
+
headers: this.sanitizeHeaders(response.headers || {}),
|
|
73
|
+
duration: `${duration}ms`,
|
|
74
|
+
dataSize: this.getDataSize(response.data),
|
|
75
|
+
url: response.config?.url,
|
|
76
|
+
method: response.config?.method?.toUpperCase(),
|
|
77
|
+
redirected: response.request?._redirectCount > 0,
|
|
78
|
+
redirectCount: response.request?._redirectCount || 0
|
|
79
|
+
},
|
|
80
|
+
performance: {
|
|
81
|
+
totalTime: duration,
|
|
82
|
+
dnsLookup: this.extractTimingInfo(response, 'lookup'),
|
|
83
|
+
tcpConnection: this.extractTimingInfo(response, 'connect'),
|
|
84
|
+
tlsHandshake: this.extractTimingInfo(response, 'secureConnect'),
|
|
85
|
+
serverProcessing: this.extractTimingInfo(response, 'response')
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.log('debug', JSON.stringify(logData, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Log HTTP errors with comprehensive diagnostic information
|
|
94
|
+
*/
|
|
95
|
+
logError(error, correlationId, startTime) {
|
|
96
|
+
if (!this.debugLogging) { return; }
|
|
97
|
+
|
|
98
|
+
const duration = startTime ? Date.now() - startTime : null;
|
|
99
|
+
const logData = {
|
|
100
|
+
type: 'HTTP_ERROR',
|
|
101
|
+
service: this.serviceName,
|
|
102
|
+
correlationId,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
error: {
|
|
105
|
+
message: error.message,
|
|
106
|
+
code: error.code,
|
|
107
|
+
errno: error.errno,
|
|
108
|
+
syscall: error.syscall,
|
|
109
|
+
status: error.response?.status,
|
|
110
|
+
statusText: error.response?.statusText,
|
|
111
|
+
headers: error.response?.headers ? this.sanitizeHeaders(error.response.headers) : undefined,
|
|
112
|
+
data: error.response?.data ? this.truncateData(error.response.data) : undefined
|
|
113
|
+
},
|
|
114
|
+
request: {
|
|
115
|
+
url: error.config?.url,
|
|
116
|
+
method: error.config?.method?.toUpperCase(),
|
|
117
|
+
timeout: error.config?.timeout,
|
|
118
|
+
duration: duration ? `${duration}ms` : 'unknown'
|
|
119
|
+
},
|
|
120
|
+
connectionInfo: this.getConnectionInfo(error),
|
|
121
|
+
troubleshooting: this.getTroubleshootingInfo(error)
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this.log('error', JSON.stringify(logData, null, 2));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sanitize headers to remove sensitive information
|
|
129
|
+
*/
|
|
130
|
+
sanitizeHeaders(headers) {
|
|
131
|
+
const sanitized = { ...headers };
|
|
132
|
+
const sensitivePatterns = [
|
|
133
|
+
'token', 'authorization', 'cookie', 'auth', 'key', 'secret', 'password'
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
Object.keys(sanitized).forEach(key => {
|
|
137
|
+
const lowerKey = key.toLowerCase();
|
|
138
|
+
if (sensitivePatterns.some(pattern => lowerKey.includes(pattern))) {
|
|
139
|
+
sanitized[key] = '[REDACTED]';
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return sanitized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get human-readable data size
|
|
148
|
+
*/
|
|
149
|
+
getDataSize(data) {
|
|
150
|
+
if (!data) { return '0 bytes'; }
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const size = typeof data === 'string' ? data.length : JSON.stringify(data).length;
|
|
154
|
+
if (size < 1024) { return `${size} bytes`; }
|
|
155
|
+
if (size < 1024 * 1024) { return `${(size / 1024).toFixed(2)} KB`; }
|
|
156
|
+
return `${(size / (1024 * 1024)).toFixed(2)} MB`;
|
|
157
|
+
} catch {
|
|
158
|
+
return 'unknown';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract connection and network diagnostic information
|
|
164
|
+
*/
|
|
165
|
+
getConnectionInfo(error) {
|
|
166
|
+
const info = {};
|
|
167
|
+
|
|
168
|
+
if (error.code) { info.errorCode = error.code; }
|
|
169
|
+
if (error.address) { info.address = error.address; }
|
|
170
|
+
if (error.port) { info.port = error.port; }
|
|
171
|
+
if (error.syscall) { info.syscall = error.syscall; }
|
|
172
|
+
if (error.errno) { info.errno = error.errno; }
|
|
173
|
+
|
|
174
|
+
// DNS resolution errors
|
|
175
|
+
if (error.code === 'ENOTFOUND') { info.dnsResolution = 'failed'; }
|
|
176
|
+
if (error.code === 'EAI_AGAIN') { info.dnsResolution = 'temporary_failure'; }
|
|
177
|
+
|
|
178
|
+
// Connection errors
|
|
179
|
+
if (error.code === 'ECONNREFUSED') { info.connection = 'refused'; }
|
|
180
|
+
if (error.code === 'ECONNRESET') { info.connection = 'reset'; }
|
|
181
|
+
if (error.code === 'ETIMEDOUT') { info.connection = 'timeout'; }
|
|
182
|
+
if (error.code === 'ECONNABORTED') { info.connection = 'aborted'; }
|
|
183
|
+
|
|
184
|
+
// TLS/SSL certificate information
|
|
185
|
+
if (error.code === 'CERT_HAS_EXPIRED') { info.tlsError = 'certificate_expired'; }
|
|
186
|
+
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') { info.tlsError = 'self_signed_certificate'; }
|
|
187
|
+
if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') { info.tlsError = 'certificate_verification_failed'; }
|
|
188
|
+
if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { info.tlsError = 'self_signed_root_certificate'; }
|
|
189
|
+
|
|
190
|
+
return Object.keys(info).length > 0 ? info : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get troubleshooting suggestions based on error type
|
|
195
|
+
*/
|
|
196
|
+
getTroubleshootingInfo(error) {
|
|
197
|
+
const suggestions = [];
|
|
198
|
+
|
|
199
|
+
switch (error.code) {
|
|
200
|
+
case 'ENOTFOUND':
|
|
201
|
+
suggestions.push('Check if the hostname is correct');
|
|
202
|
+
suggestions.push('Verify DNS resolution is working');
|
|
203
|
+
break;
|
|
204
|
+
case 'ECONNREFUSED':
|
|
205
|
+
suggestions.push('Check if the server is running');
|
|
206
|
+
suggestions.push('Verify the port number is correct');
|
|
207
|
+
suggestions.push('Check firewall rules');
|
|
208
|
+
break;
|
|
209
|
+
case 'ETIMEDOUT':
|
|
210
|
+
suggestions.push('Check network connectivity');
|
|
211
|
+
suggestions.push('Consider increasing timeout value');
|
|
212
|
+
suggestions.push('Verify server is responding');
|
|
213
|
+
break;
|
|
214
|
+
case 'CERT_HAS_EXPIRED':
|
|
215
|
+
suggestions.push('Update server certificate');
|
|
216
|
+
suggestions.push('Consider using httpsAgent with rejectUnauthorized: false for testing');
|
|
217
|
+
break;
|
|
218
|
+
case 'SELF_SIGNED_CERT_IN_CHAIN':
|
|
219
|
+
suggestions.push('Add certificate to trusted store');
|
|
220
|
+
suggestions.push('Use httpsAgent with custom CA for self-signed certificates');
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (error.response?.status === 401) {
|
|
225
|
+
suggestions.push('Check authentication credentials');
|
|
226
|
+
suggestions.push('Verify API token is valid');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (error.response?.status === 403) {
|
|
230
|
+
suggestions.push('Check user permissions');
|
|
231
|
+
suggestions.push('Verify API access is enabled');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (error.response?.status >= 500) {
|
|
235
|
+
suggestions.push('Server error - check server logs');
|
|
236
|
+
suggestions.push('Consider retry with exponential backoff');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return suggestions.length > 0 ? suggestions : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build full URL from axios config
|
|
244
|
+
*/
|
|
245
|
+
buildFullUrl(config) {
|
|
246
|
+
try {
|
|
247
|
+
const baseURL = config.baseURL || '';
|
|
248
|
+
const url = config.url || '';
|
|
249
|
+
const fullUrl = baseURL + url;
|
|
250
|
+
|
|
251
|
+
if (config.params) {
|
|
252
|
+
const params = new URLSearchParams(config.params);
|
|
253
|
+
return `${fullUrl}?${params.toString()}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return fullUrl;
|
|
257
|
+
} catch {
|
|
258
|
+
return config.url || 'unknown';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract timing information from response
|
|
264
|
+
*/
|
|
265
|
+
extractTimingInfo(response, phase) {
|
|
266
|
+
try {
|
|
267
|
+
return response.request?.connection?.[`${phase}Time`] || 'unavailable';
|
|
268
|
+
} catch {
|
|
269
|
+
return 'unavailable';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Truncate large response data for logging
|
|
275
|
+
*/
|
|
276
|
+
truncateData(data, maxLength = 1000) {
|
|
277
|
+
try {
|
|
278
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data);
|
|
279
|
+
return str.length > maxLength ? str.substring(0, maxLength) + '...[truncated]' : str;
|
|
280
|
+
} catch {
|
|
281
|
+
return '[unable to serialize]';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generic log method that can be extended for different logging backends
|
|
287
|
+
*/
|
|
288
|
+
log(level, message) {
|
|
289
|
+
switch (level) {
|
|
290
|
+
case 'debug':
|
|
291
|
+
console.debug(message);
|
|
292
|
+
break;
|
|
293
|
+
case 'info':
|
|
294
|
+
console.info(message);
|
|
295
|
+
break;
|
|
296
|
+
case 'warn':
|
|
297
|
+
console.warn(message);
|
|
298
|
+
break;
|
|
299
|
+
case 'error':
|
|
300
|
+
console.error(message);
|
|
301
|
+
break;
|
|
302
|
+
default:
|
|
303
|
+
console.log(message);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create axios instance with HTTP logging interceptors
|
|
309
|
+
*/
|
|
310
|
+
createAxiosInstance(baseConfig = {}) {
|
|
311
|
+
const axios = require('axios');
|
|
312
|
+
const instance = axios.create(baseConfig);
|
|
313
|
+
|
|
314
|
+
// Request interceptor
|
|
315
|
+
instance.interceptors.request.use(
|
|
316
|
+
(config) => {
|
|
317
|
+
const correlationId = this.generateCorrelationId();
|
|
318
|
+
const startTime = Date.now();
|
|
319
|
+
|
|
320
|
+
config.metadata = { correlationId, startTime };
|
|
321
|
+
this.logRequest(config, correlationId);
|
|
322
|
+
|
|
323
|
+
return config;
|
|
324
|
+
},
|
|
325
|
+
(error) => {
|
|
326
|
+
this.log('error', `Request interceptor error: ${error.message}`);
|
|
327
|
+
return Promise.reject(error);
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Response interceptor
|
|
332
|
+
instance.interceptors.response.use(
|
|
333
|
+
(response) => {
|
|
334
|
+
const { correlationId, startTime } = response.config.metadata || {};
|
|
335
|
+
this.logResponse(response, correlationId, startTime);
|
|
336
|
+
return response;
|
|
337
|
+
},
|
|
338
|
+
(error) => {
|
|
339
|
+
const { correlationId, startTime } = error.config?.metadata || {};
|
|
340
|
+
this.logError(error, correlationId, startTime);
|
|
341
|
+
return Promise.reject(error);
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return instance;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Wrap existing axios instance with logging
|
|
350
|
+
*/
|
|
351
|
+
wrapAxiosInstance(axiosInstance) {
|
|
352
|
+
// Request interceptor
|
|
353
|
+
axiosInstance.interceptors.request.use(
|
|
354
|
+
(config) => {
|
|
355
|
+
const correlationId = this.generateCorrelationId();
|
|
356
|
+
const startTime = Date.now();
|
|
357
|
+
|
|
358
|
+
config.metadata = { correlationId, startTime };
|
|
359
|
+
this.logRequest(config, correlationId);
|
|
360
|
+
|
|
361
|
+
return config;
|
|
362
|
+
},
|
|
363
|
+
(error) => {
|
|
364
|
+
this.log('error', `Request interceptor error: ${error.message}`);
|
|
365
|
+
return Promise.reject(error);
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// Response interceptor
|
|
370
|
+
axiosInstance.interceptors.response.use(
|
|
371
|
+
(response) => {
|
|
372
|
+
const { correlationId, startTime } = response.config.metadata || {};
|
|
373
|
+
this.logResponse(response, correlationId, startTime);
|
|
374
|
+
return response;
|
|
375
|
+
},
|
|
376
|
+
(error) => {
|
|
377
|
+
const { correlationId, startTime } = error.config?.metadata || {};
|
|
378
|
+
this.logError(error, correlationId, startTime);
|
|
379
|
+
return Promise.reject(error);
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return axiosInstance;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = { HttpLogger };
|