devlogs 2.2.6__tar.gz → 2.2.8__tar.gz

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.
Files changed (76) hide show
  1. {devlogs-2.2.6/src/devlogs.egg-info → devlogs-2.2.8}/PKG-INFO +84 -24
  2. {devlogs-2.2.6 → devlogs-2.2.8}/README.md +83 -23
  3. {devlogs-2.2.6 → devlogs-2.2.8}/pyproject.toml +1 -1
  4. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/_version_static.py +1 -1
  5. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/cli.py +17 -2
  6. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/__init__.py +18 -0
  7. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/auth.py +22 -13
  8. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/cli.py +26 -5
  9. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/errors.py +13 -0
  10. devlogs-2.2.8/src/devlogs/collector/plugins.py +189 -0
  11. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/server.py +75 -5
  12. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/config.py +12 -7
  13. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/devlogs_client.py +18 -0
  14. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/handler.py +16 -1
  15. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/mcp/server.py +53 -0
  16. {devlogs-2.2.6 → devlogs-2.2.8/src/devlogs.egg-info}/PKG-INFO +84 -24
  17. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs.egg-info/SOURCES.txt +2 -0
  18. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_cli.py +103 -0
  19. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_collector_auth.py +46 -28
  20. devlogs-2.2.8/tests/test_collector_plugins.py +626 -0
  21. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_collector_server.py +25 -2
  22. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_config.py +54 -0
  23. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_devlogs_client.py +134 -0
  24. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_handler.py +200 -0
  25. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_mcp_server.py +58 -0
  26. {devlogs-2.2.6 → devlogs-2.2.8}/LICENSE +0 -0
  27. {devlogs-2.2.6 → devlogs-2.2.8}/MANIFEST.in +0 -0
  28. {devlogs-2.2.6 → devlogs-2.2.8}/setup.cfg +0 -0
  29. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/__init__.py +0 -0
  30. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/__main__.py +0 -0
  31. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/build_info.py +0 -0
  32. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/forwarder.py +0 -0
  33. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/ingestor.py +0 -0
  34. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/collector/schema.py +0 -0
  35. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/context.py +0 -0
  36. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/demo.py +0 -0
  37. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/formatting.py +0 -0
  38. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/jenkins/__init__.py +0 -0
  39. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/jenkins/cli.py +0 -0
  40. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/jenkins/core.py +0 -0
  41. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/levels.py +0 -0
  42. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/mcp/__init__.py +0 -0
  43. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/opensearch/__init__.py +0 -0
  44. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/opensearch/client.py +0 -0
  45. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/opensearch/indexing.py +0 -0
  46. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/opensearch/mappings.py +0 -0
  47. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/opensearch/queries.py +0 -0
  48. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/retention.py +0 -0
  49. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/scrub.py +0 -0
  50. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/time_utils.py +0 -0
  51. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/version.py +0 -0
  52. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/web/__init__.py +0 -0
  53. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/web/server.py +0 -0
  54. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/web/static/devlogs.css +0 -0
  55. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/web/static/devlogs.js +0 -0
  56. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/web/static/index.html +0 -0
  57. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs/wrapper.py +0 -0
  58. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs.egg-info/dependency_links.txt +0 -0
  59. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs.egg-info/entry_points.txt +0 -0
  60. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs.egg-info/requires.txt +0 -0
  61. {devlogs-2.2.6 → devlogs-2.2.8}/src/devlogs.egg-info/top_level.txt +0 -0
  62. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_build_info.py +0 -0
  63. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_collector_config.py +0 -0
  64. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_collector_schema.py +0 -0
  65. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_context.py +0 -0
  66. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_formatting.py +0 -0
  67. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_indexing.py +0 -0
  68. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_levels.py +0 -0
  69. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_mappings.py +0 -0
  70. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_opensearch_client.py +0 -0
  71. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_opensearch_queries.py +0 -0
  72. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_retention.py +0 -0
  73. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_scrub.py +0 -0
  74. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_time_utils.py +0 -0
  75. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_url_parsing.py +0 -0
  76. {devlogs-2.2.6 → devlogs-2.2.8}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlogs
3
- Version: 2.2.6
3
+ Version: 2.2.8
4
4
  Summary: Developer-focused logging library for Python with OpenSearch integration.
5
5
  Author-email: Dan Driscoll <dan@thedandriscoll.org>
6
6
  License: MIT License
@@ -62,35 +62,61 @@ If you don't have OpenSearch running and you want to stand one up:
62
62
 
63
63
  ## Step 2: Copy/paste these instructions into your coding agent
64
64
 
65
+ ### Python
66
+
65
67
  > Please do the following in this project:
66
68
  > 1. Install `devlogs` as a dev dependency (`pip install devlogs` or add it to `requirements-dev.txt`/`pyproject.toml` optional dependencies).
67
- > 2. Add `DEVLOGS_OPENSEARCH_HOST`, `DEVLOGS_OPENSEARCH_PORT`, `DEVLOGS_OPENSEARCH_USER`, `DEVLOGS_OPENSEARCH_PASS`, and `DEVLOGS_INDEX` to the project `.env` file (or equivalent config). Defaults are `localhost`, `9200`, `admin`, and `devlogs-<projectname>`.
68
- > 3. Add devlogs hooks at the beginning of the application (main entrypoint/startup module), wrapped in an environment check so it only runs in development:
69
+ > 2. Create a `.env.devlogs` file in the project root with a single `DEVLOGS_URL` variable (devlogs auto-discovers this file). Use the `opensearchs://` scheme for TLS or `opensearch://` for non-TLS:
70
+ > ```
71
+ > DEVLOGS_URL=opensearchs://admin:YourPasswordHere@localhost:9200/devlogs-<projectname>
72
+ > ```
73
+ > 3. Run `devlogs init` (inside the virtualenv if one is set up) and verify the index is healthy.
74
+ > 4. Add devlogs hooks at the beginning of the application (main entrypoint/startup module), wrapped in an environment check so it only runs in development:
69
75
  > ```python
70
76
  > import os
71
77
  > import logging
72
78
  > if os.getenv("ENVIRONMENT") != "production":
73
79
  > from devlogs.handler import DevlogsHandler
80
+ > from devlogs.opensearch.client import get_opensearch_client
74
81
  > from devlogs.build_info import resolve_build_info
75
82
  >
76
- > # Resolve build info (reads .build.json or generates)
77
83
  > build_info = resolve_build_info(write_if_missing=True)
78
- >
79
84
  > handler = DevlogsHandler(
80
85
  > application="my-app", # Required: your app name
81
86
  > component="api", # Required: component name
82
87
  > level=logging.INFO,
88
+ > opensearch_client=get_opensearch_client(),
83
89
  > version=build_info.build_id,
84
90
  > )
85
91
  > logging.getLogger().addHandler(handler)
86
92
  > logging.getLogger().setLevel(logging.INFO)
87
- >
88
- > # Log startup
89
93
  > logging.info("App started")
90
94
  > ```
91
- > 4. Run `devlogs init` (inside the virtualenv if one is set up) and verify the index is healthy.
92
95
  > 5. Ask the user if they want MCP set up; if yes, state which agent you are (`copilot`, `claude`, or `codex`) and run `devlogs initmcp <agent>`.
93
96
 
97
+ ### JavaScript / TypeScript (browser)
98
+
99
+ > Please do the following in this project:
100
+ > 1. Install the browser package as a dev dependency: `npm install --save-dev devlogs-browser`.
101
+ > 2. Initialize devlogs early in the application entrypoint, wrapped in an environment check so it only runs in development:
102
+ > ```javascript
103
+ > import * as devlogs from 'devlogs-browser';
104
+ >
105
+ > if (process.env.NODE_ENV === 'development') {
106
+ > devlogs.init({
107
+ > url: 'https://admin:YourPasswordHere@localhost:9200',
108
+ > index: 'devlogs-<projectname>',
109
+ > application: 'my-app', // Required: your app name
110
+ > component: 'frontend', // Required: component name
111
+ > });
112
+ > }
113
+ > ```
114
+ > After `init()`, all `console.log`, `console.warn`, `console.error`, and `console.debug` calls are automatically forwarded to OpenSearch. The original console output is preserved.
115
+ > 3. Use `devlogs.setArea('dashboard')` and `devlogs.setOperationId('op-123')` to add context to logs. Pass a plain object as the last argument to attach custom fields:
116
+ > ```javascript
117
+ > console.log('User action', { userId: 123, action: 'clicked' });
118
+ > ```
119
+
94
120
  ## Step 3: Use devlogs
95
121
 
96
122
  1. Run `devlogs initmcp <agent>` to set up the MCP server.
@@ -108,14 +134,28 @@ If you don't have OpenSearch running and you want to stand one up:
108
134
  ```sh
109
135
  docker-compose up -d opensearch
110
136
  ```
111
- Or point `DEVLOGS_OPENSEARCH_*` at an existing cluster.
137
+ Or point `DEVLOGS_URL` at an existing cluster.
112
138
 
113
- 3. **Initialize indices/templates:**
139
+ 3. **Configure connection** (choose one):
140
+
141
+ Option A — `.env.devlogs` file (auto-discovered):
142
+ ```
143
+ DEVLOGS_URL=opensearchs://admin:YourPasswordHere@localhost:9200/devlogs-myproject
144
+ ```
145
+
146
+ Option B — `--url` flag (no config file needed):
147
+ ```sh
148
+ devlogs --url 'opensearchs://admin:pass@localhost:9200/devlogs-myproject' init
149
+ ```
150
+
151
+ Use `devlogs mkurl` to interactively build a properly URL-encoded connection string (handy for passwords with special characters).
152
+
153
+ 4. **Initialize indices/templates:**
114
154
  ```sh
115
155
  devlogs init
116
156
  ```
117
157
 
118
- 4. **Use in Python code (development only):**
158
+ 5. **Use in Python code (development only):**
119
159
  ```python
120
160
  import os
121
161
  import logging
@@ -123,15 +163,15 @@ If you don't have OpenSearch running and you want to stand one up:
123
163
  # Only enable devlogs in development
124
164
  if os.getenv("ENVIRONMENT") != "production":
125
165
  from devlogs.handler import DevlogsHandler
166
+ from devlogs.opensearch.client import get_opensearch_client
126
167
  from devlogs.build_info import resolve_build_info
127
168
 
128
- # Get build info (reads .build.json or generates)
129
169
  build_info = resolve_build_info(write_if_missing=True)
130
-
131
170
  handler = DevlogsHandler(
132
171
  application="my-app",
133
172
  component="default",
134
173
  level=logging.DEBUG,
174
+ opensearch_client=get_opensearch_client(),
135
175
  version=build_info.build_id,
136
176
  )
137
177
  logging.getLogger().addHandler(handler)
@@ -140,17 +180,38 @@ If you don't have OpenSearch running and you want to stand one up:
140
180
  logging.info("Hello from devlogs!")
141
181
  ```
142
182
 
143
- 5. **Tail logs from CLI:**
183
+ 6. **Use in JavaScript/TypeScript (browser, development only):**
184
+ ```javascript
185
+ import * as devlogs from 'devlogs-browser';
186
+
187
+ if (process.env.NODE_ENV === 'development') {
188
+ devlogs.init({
189
+ url: 'https://admin:YourPasswordHere@localhost:9200',
190
+ index: 'devlogs-myproject',
191
+ application: 'my-app',
192
+ component: 'frontend',
193
+ });
194
+ }
195
+
196
+ // All console methods are now forwarded to OpenSearch
197
+ console.log('Hello from devlogs!');
198
+
199
+ // Add context
200
+ devlogs.setArea('dashboard');
201
+ console.log('User action', { userId: 123, action: 'clicked' });
202
+ ```
203
+
204
+ 7. **Tail logs from CLI:**
144
205
  ```sh
145
206
  devlogs tail --area web --follow
146
207
  ```
147
208
 
148
- 6. **Search logs from CLI:**
209
+ 8. **Search logs from CLI:**
149
210
  ```sh
150
211
  devlogs search --q "error" --area web
151
212
  ```
152
213
 
153
- 7. **Run the web UI:**
214
+ 9. **Run the web UI:**
154
215
  ```sh
155
216
  uvicorn devlogs.web.server:app --port 8088
156
217
  # Then open http://localhost:8088/ui/
@@ -224,7 +285,7 @@ client.emit(
224
285
 
225
286
  ```bash
226
287
  docker build -f Dockerfile.collector -t devlogs-collector .
227
- docker run -p 8080:8080 -e DEVLOGS_OPENSEARCH_URL=https://admin:pass@opensearch:9200/devlogs devlogs-collector
288
+ docker run -p 8080:8080 -e DEVLOGS_URL=opensearchs://admin:pass@opensearch:9200/devlogs devlogs-collector
228
289
  ```
229
290
 
230
291
  See [HOWTO-COLLECTOR.md](HOWTO-COLLECTOR.md) for complete collector documentation.
@@ -260,7 +321,7 @@ Stream Jenkins build logs to OpenSearch using a standalone binary:
260
321
  pipeline {
261
322
  agent any
262
323
  environment {
263
- DEVLOGS_OPENSEARCH_URL = credentials('devlogs-opensearch-url')
324
+ DEVLOGS_URL = credentials('devlogs-url')
264
325
  DEVLOGS_BINARY_URL = credentials('devlogs-binary-url')
265
326
  }
266
327
  stages {
@@ -284,13 +345,12 @@ Build the binary with `./build-standalone.sh` and host it somewhere accessible.
284
345
 
285
346
  ### Environment Variables
286
347
 
287
- **Collector Configuration:**
288
- - `DEVLOGS_URL` - Collector base URL (where apps send logs)
348
+ **Connection (choose one):**
349
+ - `DEVLOGS_URL` - Standard connection URL with auto-detection. OpenSearch URLs (`opensearchs://`, `opensearch://`) connect directly; collector URLs (`http://`, `https://`) use the collector endpoint.
289
350
  - `DEVLOGS_FORWARD_URL` - Forward mode: proxy to this upstream URL
290
351
 
291
- **OpenSearch Admin Connection:**
352
+ **OpenSearch (individual variables, alternative to DEVLOGS_URL):**
292
353
  - `DEVLOGS_OPENSEARCH_HOST`, `DEVLOGS_OPENSEARCH_PORT`, `DEVLOGS_OPENSEARCH_USER`, `DEVLOGS_OPENSEARCH_PASS`
293
- - `DEVLOGS_OPENSEARCH_URL` - URL shortcut (e.g., `https://user:pass@host:9200/index`)
294
354
  - `DEVLOGS_OPENSEARCH_VERIFY_CERTS`, `DEVLOGS_OPENSEARCH_CA_CERT` - SSL/TLS settings
295
355
 
296
356
  **Index & Retention:**
@@ -308,7 +368,7 @@ See [.env.example](.env.example) for a complete configuration template.
308
368
  Use `--url` to specify connection details without a `.env` file:
309
369
 
310
370
  ```bash
311
- devlogs --url 'https://admin:pass@host:9200/myindex' tail
371
+ devlogs --url 'opensearchs://admin:pass@host:9200/myindex' tail
312
372
  ```
313
373
 
314
374
  Use `--env` to load from a specific `.env` file:
@@ -325,7 +385,7 @@ Use `devlogs mkurl` to interactively create a properly URL-encoded connection st
325
385
  devlogs mkurl
326
386
  # Outputs the URL in three formats:
327
387
  # 1. Bare URL (for --url flag)
328
- # 2. Single DEVLOGS_OPENSEARCH_URL variable
388
+ # 2. Single DEVLOGS_URL variable
329
389
  # 3. Individual .env variables
330
390
  ```
331
391
 
@@ -19,35 +19,61 @@ If you don't have OpenSearch running and you want to stand one up:
19
19
 
20
20
  ## Step 2: Copy/paste these instructions into your coding agent
21
21
 
22
+ ### Python
23
+
22
24
  > Please do the following in this project:
23
25
  > 1. Install `devlogs` as a dev dependency (`pip install devlogs` or add it to `requirements-dev.txt`/`pyproject.toml` optional dependencies).
24
- > 2. Add `DEVLOGS_OPENSEARCH_HOST`, `DEVLOGS_OPENSEARCH_PORT`, `DEVLOGS_OPENSEARCH_USER`, `DEVLOGS_OPENSEARCH_PASS`, and `DEVLOGS_INDEX` to the project `.env` file (or equivalent config). Defaults are `localhost`, `9200`, `admin`, and `devlogs-<projectname>`.
25
- > 3. Add devlogs hooks at the beginning of the application (main entrypoint/startup module), wrapped in an environment check so it only runs in development:
26
+ > 2. Create a `.env.devlogs` file in the project root with a single `DEVLOGS_URL` variable (devlogs auto-discovers this file). Use the `opensearchs://` scheme for TLS or `opensearch://` for non-TLS:
27
+ > ```
28
+ > DEVLOGS_URL=opensearchs://admin:YourPasswordHere@localhost:9200/devlogs-<projectname>
29
+ > ```
30
+ > 3. Run `devlogs init` (inside the virtualenv if one is set up) and verify the index is healthy.
31
+ > 4. Add devlogs hooks at the beginning of the application (main entrypoint/startup module), wrapped in an environment check so it only runs in development:
26
32
  > ```python
27
33
  > import os
28
34
  > import logging
29
35
  > if os.getenv("ENVIRONMENT") != "production":
30
36
  > from devlogs.handler import DevlogsHandler
37
+ > from devlogs.opensearch.client import get_opensearch_client
31
38
  > from devlogs.build_info import resolve_build_info
32
39
  >
33
- > # Resolve build info (reads .build.json or generates)
34
40
  > build_info = resolve_build_info(write_if_missing=True)
35
- >
36
41
  > handler = DevlogsHandler(
37
42
  > application="my-app", # Required: your app name
38
43
  > component="api", # Required: component name
39
44
  > level=logging.INFO,
45
+ > opensearch_client=get_opensearch_client(),
40
46
  > version=build_info.build_id,
41
47
  > )
42
48
  > logging.getLogger().addHandler(handler)
43
49
  > logging.getLogger().setLevel(logging.INFO)
44
- >
45
- > # Log startup
46
50
  > logging.info("App started")
47
51
  > ```
48
- > 4. Run `devlogs init` (inside the virtualenv if one is set up) and verify the index is healthy.
49
52
  > 5. Ask the user if they want MCP set up; if yes, state which agent you are (`copilot`, `claude`, or `codex`) and run `devlogs initmcp <agent>`.
50
53
 
54
+ ### JavaScript / TypeScript (browser)
55
+
56
+ > Please do the following in this project:
57
+ > 1. Install the browser package as a dev dependency: `npm install --save-dev devlogs-browser`.
58
+ > 2. Initialize devlogs early in the application entrypoint, wrapped in an environment check so it only runs in development:
59
+ > ```javascript
60
+ > import * as devlogs from 'devlogs-browser';
61
+ >
62
+ > if (process.env.NODE_ENV === 'development') {
63
+ > devlogs.init({
64
+ > url: 'https://admin:YourPasswordHere@localhost:9200',
65
+ > index: 'devlogs-<projectname>',
66
+ > application: 'my-app', // Required: your app name
67
+ > component: 'frontend', // Required: component name
68
+ > });
69
+ > }
70
+ > ```
71
+ > After `init()`, all `console.log`, `console.warn`, `console.error`, and `console.debug` calls are automatically forwarded to OpenSearch. The original console output is preserved.
72
+ > 3. Use `devlogs.setArea('dashboard')` and `devlogs.setOperationId('op-123')` to add context to logs. Pass a plain object as the last argument to attach custom fields:
73
+ > ```javascript
74
+ > console.log('User action', { userId: 123, action: 'clicked' });
75
+ > ```
76
+
51
77
  ## Step 3: Use devlogs
52
78
 
53
79
  1. Run `devlogs initmcp <agent>` to set up the MCP server.
@@ -65,14 +91,28 @@ If you don't have OpenSearch running and you want to stand one up:
65
91
  ```sh
66
92
  docker-compose up -d opensearch
67
93
  ```
68
- Or point `DEVLOGS_OPENSEARCH_*` at an existing cluster.
94
+ Or point `DEVLOGS_URL` at an existing cluster.
69
95
 
70
- 3. **Initialize indices/templates:**
96
+ 3. **Configure connection** (choose one):
97
+
98
+ Option A — `.env.devlogs` file (auto-discovered):
99
+ ```
100
+ DEVLOGS_URL=opensearchs://admin:YourPasswordHere@localhost:9200/devlogs-myproject
101
+ ```
102
+
103
+ Option B — `--url` flag (no config file needed):
104
+ ```sh
105
+ devlogs --url 'opensearchs://admin:pass@localhost:9200/devlogs-myproject' init
106
+ ```
107
+
108
+ Use `devlogs mkurl` to interactively build a properly URL-encoded connection string (handy for passwords with special characters).
109
+
110
+ 4. **Initialize indices/templates:**
71
111
  ```sh
72
112
  devlogs init
73
113
  ```
74
114
 
75
- 4. **Use in Python code (development only):**
115
+ 5. **Use in Python code (development only):**
76
116
  ```python
77
117
  import os
78
118
  import logging
@@ -80,15 +120,15 @@ If you don't have OpenSearch running and you want to stand one up:
80
120
  # Only enable devlogs in development
81
121
  if os.getenv("ENVIRONMENT") != "production":
82
122
  from devlogs.handler import DevlogsHandler
123
+ from devlogs.opensearch.client import get_opensearch_client
83
124
  from devlogs.build_info import resolve_build_info
84
125
 
85
- # Get build info (reads .build.json or generates)
86
126
  build_info = resolve_build_info(write_if_missing=True)
87
-
88
127
  handler = DevlogsHandler(
89
128
  application="my-app",
90
129
  component="default",
91
130
  level=logging.DEBUG,
131
+ opensearch_client=get_opensearch_client(),
92
132
  version=build_info.build_id,
93
133
  )
94
134
  logging.getLogger().addHandler(handler)
@@ -97,17 +137,38 @@ If you don't have OpenSearch running and you want to stand one up:
97
137
  logging.info("Hello from devlogs!")
98
138
  ```
99
139
 
100
- 5. **Tail logs from CLI:**
140
+ 6. **Use in JavaScript/TypeScript (browser, development only):**
141
+ ```javascript
142
+ import * as devlogs from 'devlogs-browser';
143
+
144
+ if (process.env.NODE_ENV === 'development') {
145
+ devlogs.init({
146
+ url: 'https://admin:YourPasswordHere@localhost:9200',
147
+ index: 'devlogs-myproject',
148
+ application: 'my-app',
149
+ component: 'frontend',
150
+ });
151
+ }
152
+
153
+ // All console methods are now forwarded to OpenSearch
154
+ console.log('Hello from devlogs!');
155
+
156
+ // Add context
157
+ devlogs.setArea('dashboard');
158
+ console.log('User action', { userId: 123, action: 'clicked' });
159
+ ```
160
+
161
+ 7. **Tail logs from CLI:**
101
162
  ```sh
102
163
  devlogs tail --area web --follow
103
164
  ```
104
165
 
105
- 6. **Search logs from CLI:**
166
+ 8. **Search logs from CLI:**
106
167
  ```sh
107
168
  devlogs search --q "error" --area web
108
169
  ```
109
170
 
110
- 7. **Run the web UI:**
171
+ 9. **Run the web UI:**
111
172
  ```sh
112
173
  uvicorn devlogs.web.server:app --port 8088
113
174
  # Then open http://localhost:8088/ui/
@@ -181,7 +242,7 @@ client.emit(
181
242
 
182
243
  ```bash
183
244
  docker build -f Dockerfile.collector -t devlogs-collector .
184
- docker run -p 8080:8080 -e DEVLOGS_OPENSEARCH_URL=https://admin:pass@opensearch:9200/devlogs devlogs-collector
245
+ docker run -p 8080:8080 -e DEVLOGS_URL=opensearchs://admin:pass@opensearch:9200/devlogs devlogs-collector
185
246
  ```
186
247
 
187
248
  See [HOWTO-COLLECTOR.md](HOWTO-COLLECTOR.md) for complete collector documentation.
@@ -217,7 +278,7 @@ Stream Jenkins build logs to OpenSearch using a standalone binary:
217
278
  pipeline {
218
279
  agent any
219
280
  environment {
220
- DEVLOGS_OPENSEARCH_URL = credentials('devlogs-opensearch-url')
281
+ DEVLOGS_URL = credentials('devlogs-url')
221
282
  DEVLOGS_BINARY_URL = credentials('devlogs-binary-url')
222
283
  }
223
284
  stages {
@@ -241,13 +302,12 @@ Build the binary with `./build-standalone.sh` and host it somewhere accessible.
241
302
 
242
303
  ### Environment Variables
243
304
 
244
- **Collector Configuration:**
245
- - `DEVLOGS_URL` - Collector base URL (where apps send logs)
305
+ **Connection (choose one):**
306
+ - `DEVLOGS_URL` - Standard connection URL with auto-detection. OpenSearch URLs (`opensearchs://`, `opensearch://`) connect directly; collector URLs (`http://`, `https://`) use the collector endpoint.
246
307
  - `DEVLOGS_FORWARD_URL` - Forward mode: proxy to this upstream URL
247
308
 
248
- **OpenSearch Admin Connection:**
309
+ **OpenSearch (individual variables, alternative to DEVLOGS_URL):**
249
310
  - `DEVLOGS_OPENSEARCH_HOST`, `DEVLOGS_OPENSEARCH_PORT`, `DEVLOGS_OPENSEARCH_USER`, `DEVLOGS_OPENSEARCH_PASS`
250
- - `DEVLOGS_OPENSEARCH_URL` - URL shortcut (e.g., `https://user:pass@host:9200/index`)
251
311
  - `DEVLOGS_OPENSEARCH_VERIFY_CERTS`, `DEVLOGS_OPENSEARCH_CA_CERT` - SSL/TLS settings
252
312
 
253
313
  **Index & Retention:**
@@ -265,7 +325,7 @@ See [.env.example](.env.example) for a complete configuration template.
265
325
  Use `--url` to specify connection details without a `.env` file:
266
326
 
267
327
  ```bash
268
- devlogs --url 'https://admin:pass@host:9200/myindex' tail
328
+ devlogs --url 'opensearchs://admin:pass@host:9200/myindex' tail
269
329
  ```
270
330
 
271
331
  Use `--env` to load from a specific `.env` file:
@@ -282,7 +342,7 @@ Use `devlogs mkurl` to interactively create a properly URL-encoded connection st
282
342
  devlogs mkurl
283
343
  # Outputs the URL in three formats:
284
344
  # 1. Bare URL (for --url flag)
285
- # 2. Single DEVLOGS_OPENSEARCH_URL variable
345
+ # 2. Single DEVLOGS_URL variable
286
346
  # 3. Individual .env variables
287
347
  ```
288
348
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlogs"
7
- version = "2.2.6"
7
+ version = "2.2.8"
8
8
  description = "Developer-focused logging library for Python with OpenSearch integration."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -1,2 +1,2 @@
1
1
  # AUTO-GENERATED at build time — do not edit or commit
2
- __version__ = "2.2.5"
2
+ __version__ = "2.2.8"
@@ -600,14 +600,29 @@ def diagnose(
600
600
  else:
601
601
  _emit("warn", ".env: not found and no DEVLOGS_* settings detected")
602
602
 
603
+ # Plugin / collector mode check
604
+ if cfg.url_mode == "collector":
605
+ from .collector.plugins import get_plugin_for_url
606
+ plugin = get_plugin_for_url(cfg.collector_url, cfg)
607
+ if plugin:
608
+ _emit("ok", f"Mode: plugin ({plugin.name})")
609
+ try:
610
+ status = plugin.check()
611
+ _emit("ok", f"Plugin: {status}")
612
+ except Exception as e:
613
+ _emit("error", f"Plugin: {e}")
614
+ else:
615
+ _emit("ok", f"Mode: collector ({cfg.collector_url})")
616
+
603
617
  client = None
604
618
  connection_ok = False
619
+ os_severity = "warn" if cfg.url_mode == "collector" else "error"
605
620
  try:
606
621
  client = get_opensearch_client()
607
622
  except DevlogsDisabledError as exc:
608
- _emit("error", f"OpenSearch: {exc}")
623
+ _emit(os_severity, f"OpenSearch: {exc}")
609
624
  except OpenSearchError as exc:
610
- _emit("error", f"OpenSearch: {exc}")
625
+ _emit(os_severity, f"OpenSearch: {exc}")
611
626
  else:
612
627
  try:
613
628
  check_connection(client)
@@ -13,6 +13,7 @@ from .errors import (
13
13
  ValidationError,
14
14
  ForwardError,
15
15
  IngestError,
16
+ PluginError,
16
17
  ConfigurationError,
17
18
  error_response,
18
19
  map_upstream_error,
@@ -22,8 +23,17 @@ from .errors import (
22
23
  ERROR_INVALID_PAYLOAD,
23
24
  ERROR_FORWARD_FAILED,
24
25
  ERROR_INGEST_FAILED,
26
+ ERROR_PLUGIN_FAILED,
25
27
  ERROR_NOT_CONFIGURED,
26
28
  )
29
+ from .plugins import (
30
+ OutputPlugin,
31
+ register_plugin,
32
+ get_plugin_for_url,
33
+ get_registered_schemes,
34
+ list_plugins,
35
+ dict_to_record,
36
+ )
27
37
 
28
38
  __all__ = [
29
39
  "DevlogsRecord",
@@ -34,6 +44,7 @@ __all__ = [
34
44
  "ValidationError",
35
45
  "ForwardError",
36
46
  "IngestError",
47
+ "PluginError",
37
48
  "ConfigurationError",
38
49
  "CollectorError",
39
50
  "error_response",
@@ -44,5 +55,12 @@ __all__ = [
44
55
  "ERROR_INVALID_PAYLOAD",
45
56
  "ERROR_FORWARD_FAILED",
46
57
  "ERROR_INGEST_FAILED",
58
+ "ERROR_PLUGIN_FAILED",
47
59
  "ERROR_NOT_CONFIGURED",
60
+ "OutputPlugin",
61
+ "register_plugin",
62
+ "get_plugin_for_url",
63
+ "get_registered_schemes",
64
+ "list_plugins",
65
+ "dict_to_record",
48
66
  ]
@@ -15,7 +15,6 @@ from urllib.parse import unquote
15
15
  TOKEN_PATTERN = re.compile(r"^.{8,}$")
16
16
 
17
17
  # Auth header patterns
18
- DEVLOGS1_PATTERN = re.compile(r"^Devlogs1\s+(\S+)", re.IGNORECASE)
19
18
  BEARER_PATTERN = re.compile(r"^Bearer\s+(\S+)", re.IGNORECASE)
20
19
 
21
20
  # Auth modes
@@ -117,44 +116,54 @@ def is_token_well_formed(token: Optional[str]) -> bool:
117
116
  return bool(TOKEN_PATTERN.match(token))
118
117
 
119
118
 
120
- def extract_token_from_headers(
119
+ def extract_token(
121
120
  authorization: Optional[str] = None,
122
121
  x_devlogs_token: Optional[str] = None,
122
+ url_userinfo: Optional[str] = None,
123
+ url_query_token: Optional[str] = None,
123
124
  ) -> Tuple[Optional[str], str]:
124
- """Extract token from request headers with proper precedence.
125
+ """Extract token from request with proper precedence.
125
126
 
126
- Precedence: Devlogs1 Bearer → X-Devlogs-Token
127
+ Precedence: Bearer header → X-Devlogs-Token header → URL userinfo → ?token= query
127
128
 
128
129
  Args:
129
130
  authorization: The Authorization header value
130
131
  x_devlogs_token: The X-Devlogs-Token header value
132
+ url_userinfo: Username from URL (e.g. token@host)
133
+ url_query_token: The ?token= query parameter value
131
134
 
132
135
  Returns:
133
136
  Tuple of (token_value, source) where source is one of:
134
- 'devlogs1', 'bearer', 'x-devlogs-token', or 'none'
137
+ 'bearer', 'x-devlogs-token', 'url-userinfo', 'url-query', or 'none'
135
138
  """
136
139
  if authorization:
137
140
  authorization = authorization.strip()
138
-
139
- # Try Devlogs1 first
140
- match = DEVLOGS1_PATTERN.match(authorization)
141
- if match:
142
- return match.group(1), "devlogs1"
143
-
144
- # Try Bearer
145
141
  match = BEARER_PATTERN.match(authorization)
146
142
  if match:
147
143
  return match.group(1), "bearer"
148
144
 
149
- # Fall back to X-Devlogs-Token
150
145
  if x_devlogs_token:
151
146
  token = x_devlogs_token.strip()
152
147
  if token:
153
148
  return token, "x-devlogs-token"
154
149
 
150
+ if url_userinfo:
151
+ token = unquote(url_userinfo.strip())
152
+ if token:
153
+ return token, "url-userinfo"
154
+
155
+ if url_query_token:
156
+ token = unquote(url_query_token.strip())
157
+ if token:
158
+ return token, "url-query"
159
+
155
160
  return None, "none"
156
161
 
157
162
 
163
+ # Keep old name as alias for backwards compatibility
164
+ extract_token_from_headers = extract_token
165
+
166
+
158
167
  def parse_token_map_kv(kv_string: Optional[str]) -> Dict[str, TokenMapping]:
159
168
  """Parse the DEVLOGS_TOKEN_MAP_KV environment variable.
160
169
 
@@ -67,11 +67,17 @@ def serve(
67
67
  fg=typer.colors.YELLOW
68
68
  ))
69
69
  else:
70
- mode_str = "FORWARD" if mode == "forward" else "INGEST"
71
- typer.echo(typer.style(f"Starting collector in {mode_str} mode", fg=typer.colors.GREEN))
72
70
  if mode == "forward":
73
- typer.echo(f" Forwarding to: {cfg.forward_url}")
71
+ from .plugins import get_plugin_for_url
72
+ plugin = get_plugin_for_url(cfg.forward_url, cfg)
73
+ if plugin:
74
+ typer.echo(typer.style(f"Starting collector in PLUGIN mode ({plugin.name})", fg=typer.colors.GREEN))
75
+ typer.echo(f" {plugin.display_info()}")
76
+ else:
77
+ typer.echo(typer.style("Starting collector in FORWARD mode", fg=typer.colors.GREEN))
78
+ typer.echo(f" Forwarding to: {cfg.forward_url}")
74
79
  else:
80
+ typer.echo(typer.style("Starting collector in INGEST mode", fg=typer.colors.GREEN))
75
81
  typer.echo(f" OpenSearch: {cfg.opensearch_host}:{cfg.opensearch_port}")
76
82
  typer.echo(f" Index: {cfg.index}")
77
83
 
@@ -107,13 +113,20 @@ def check():
107
113
  typer.echo()
108
114
 
109
115
  # Show mode
116
+ plugin = None
110
117
  if mode == "error":
111
118
  typer.echo(typer.style("Mode: NOT CONFIGURED", fg=typer.colors.RED))
112
119
  typer.echo(" Set DEVLOGS_FORWARD_URL or DEVLOGS_OPENSEARCH_* variables")
113
120
  raise typer.Exit(1)
114
121
  elif mode == "forward":
115
- typer.echo(typer.style("Mode: FORWARD", fg=typer.colors.CYAN))
116
- typer.echo(f" Forward URL: {cfg.forward_url}")
122
+ from .plugins import get_plugin_for_url
123
+ plugin = get_plugin_for_url(cfg.forward_url, cfg)
124
+ if plugin:
125
+ typer.echo(typer.style(f"Mode: PLUGIN ({plugin.name})", fg=typer.colors.CYAN))
126
+ typer.echo(f" {plugin.display_info()}")
127
+ else:
128
+ typer.echo(typer.style("Mode: FORWARD", fg=typer.colors.CYAN))
129
+ typer.echo(f" Forward URL: {cfg.forward_url}")
117
130
  else:
118
131
  typer.echo(typer.style("Mode: INGEST", fg=typer.colors.CYAN))
119
132
  typer.echo(f" OpenSearch Host: {cfg.opensearch_host}")
@@ -146,6 +159,14 @@ def check():
146
159
  except Exception as e:
147
160
  typer.echo(typer.style(f" OpenSearch: FAILED - {e}", fg=typer.colors.RED))
148
161
  raise typer.Exit(1)
162
+ elif plugin:
163
+ typer.echo(f"Testing {plugin.name} connectivity...")
164
+ try:
165
+ status = plugin.check()
166
+ typer.echo(typer.style(f" {status}", fg=typer.colors.GREEN))
167
+ except Exception as e:
168
+ typer.echo(typer.style(f" Plugin check failed: {e}", fg=typer.colors.RED))
169
+ raise typer.Exit(1)
149
170
  else:
150
171
  typer.echo("Testing forward URL connectivity...")
151
172
  try: