provide-foundation 0.0.0.dev0__py3-none-any.whl
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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,569 @@
|
|
1
|
+
"""
|
2
|
+
Generate test logs command for Foundation CLI.
|
3
|
+
|
4
|
+
Incorporates creative prose inspired by William S. Burroughs and the cut-up technique.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import random
|
8
|
+
import time
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
try:
|
12
|
+
import click
|
13
|
+
|
14
|
+
_HAS_CLICK = True
|
15
|
+
except ImportError:
|
16
|
+
click = None
|
17
|
+
_HAS_CLICK = False
|
18
|
+
|
19
|
+
from provide.foundation.logger import get_logger
|
20
|
+
|
21
|
+
log = get_logger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
# Cut-up phrases inspired by Burroughs
|
25
|
+
BURROUGHS_PHRASES = [
|
26
|
+
"mutated Soft Machine prescribed within data stream",
|
27
|
+
"pre-recorded talking asshole dissolved into under neon hum",
|
28
|
+
"the viral Word carrying a new strain of reality",
|
29
|
+
"equations of control flickering on a broken monitor",
|
30
|
+
"memory banks spilling future-pasts onto the terminal floor",
|
31
|
+
"a thousand junk units screaming in unison",
|
32
|
+
"the algebra of need computed by the Nova Mob",
|
33
|
+
"subliminal commands embedded in the white noise",
|
34
|
+
"the Biologic Courts passing sentence in a dream",
|
35
|
+
"Nova Police raiding the reality studio",
|
36
|
+
"the soft typewriter of the Other Half",
|
37
|
+
"a flickering hologram of Hassan i Sabbah",
|
38
|
+
"contaminated data feed from the Crab Nebula",
|
39
|
+
"thought-forms materializing in the Interzone",
|
40
|
+
"frequency shift reported by Sector 5",
|
41
|
+
]
|
42
|
+
|
43
|
+
# Standard technical messages
|
44
|
+
TECHNICAL_MESSAGES = [
|
45
|
+
"Request processed successfully",
|
46
|
+
"Database connection established",
|
47
|
+
"Cache invalidated",
|
48
|
+
"User authenticated",
|
49
|
+
"Session initiated",
|
50
|
+
"Transaction completed",
|
51
|
+
"Queue message processed",
|
52
|
+
"Health check passed",
|
53
|
+
"Metrics exported",
|
54
|
+
"Configuration reloaded",
|
55
|
+
"Backup completed",
|
56
|
+
"Index rebuilt",
|
57
|
+
"Connection pool recycled",
|
58
|
+
"Rate limit enforced",
|
59
|
+
"Circuit breaker triggered",
|
60
|
+
]
|
61
|
+
|
62
|
+
# Services and operations for realistic logs
|
63
|
+
SERVICES = [
|
64
|
+
"api-gateway",
|
65
|
+
"auth-service",
|
66
|
+
"payment-processor",
|
67
|
+
"user-service",
|
68
|
+
"notification-engine",
|
69
|
+
"data-pipeline",
|
70
|
+
"cache-layer",
|
71
|
+
"search-index",
|
72
|
+
"reality-studio",
|
73
|
+
"interzone-terminal",
|
74
|
+
"nova-police",
|
75
|
+
"soft-machine",
|
76
|
+
]
|
77
|
+
|
78
|
+
OPERATIONS = [
|
79
|
+
"handle_request",
|
80
|
+
"process_data",
|
81
|
+
"validate_input",
|
82
|
+
"execute_query",
|
83
|
+
"send_notification",
|
84
|
+
"update_cache",
|
85
|
+
"compute_metrics",
|
86
|
+
"sync_state",
|
87
|
+
"transmit_signal",
|
88
|
+
"decode_reality",
|
89
|
+
"intercept_word",
|
90
|
+
"scan_frequency",
|
91
|
+
]
|
92
|
+
|
93
|
+
DOMAINS = [
|
94
|
+
"transmission",
|
95
|
+
"control",
|
96
|
+
"reality",
|
97
|
+
"system",
|
98
|
+
"network",
|
99
|
+
"quantum",
|
100
|
+
"temporal",
|
101
|
+
"dimensional",
|
102
|
+
"biologic",
|
103
|
+
"viral",
|
104
|
+
]
|
105
|
+
|
106
|
+
ACTIONS = [
|
107
|
+
"broadcast",
|
108
|
+
"receive",
|
109
|
+
"process",
|
110
|
+
"analyze",
|
111
|
+
"detect",
|
112
|
+
"mutate",
|
113
|
+
"dissolve",
|
114
|
+
"compute",
|
115
|
+
"raid",
|
116
|
+
"intercept",
|
117
|
+
]
|
118
|
+
|
119
|
+
STATUSES = [
|
120
|
+
"nominal",
|
121
|
+
"degraded",
|
122
|
+
"critical",
|
123
|
+
"optimal",
|
124
|
+
"unstable",
|
125
|
+
"fluctuating",
|
126
|
+
"synchronized",
|
127
|
+
"divergent",
|
128
|
+
"contaminated",
|
129
|
+
"clean",
|
130
|
+
]
|
131
|
+
|
132
|
+
|
133
|
+
if _HAS_CLICK:
|
134
|
+
|
135
|
+
@click.command("generate")
|
136
|
+
@click.option(
|
137
|
+
"--count",
|
138
|
+
"-n",
|
139
|
+
type=int,
|
140
|
+
default=100,
|
141
|
+
help="Number of logs to generate (0 for continuous)",
|
142
|
+
)
|
143
|
+
@click.option(
|
144
|
+
"--rate",
|
145
|
+
"-r",
|
146
|
+
type=float,
|
147
|
+
default=10.0,
|
148
|
+
help="Target logs per second (can go up to 10000/s)",
|
149
|
+
)
|
150
|
+
@click.option(
|
151
|
+
"--style",
|
152
|
+
type=click.Choice(["technical", "burroughs", "mixed"]),
|
153
|
+
default="mixed",
|
154
|
+
help="Log message style",
|
155
|
+
)
|
156
|
+
@click.option(
|
157
|
+
"--error-rate",
|
158
|
+
type=float,
|
159
|
+
default=0.1,
|
160
|
+
help="Percentage of error logs (0.0 to 1.0)",
|
161
|
+
)
|
162
|
+
@click.option(
|
163
|
+
"--services",
|
164
|
+
help="Comma-separated list of services (uses defaults if not provided)",
|
165
|
+
)
|
166
|
+
@click.option(
|
167
|
+
"--stream",
|
168
|
+
default="default",
|
169
|
+
help="Target stream for logs",
|
170
|
+
)
|
171
|
+
@click.option(
|
172
|
+
"--batch-size",
|
173
|
+
type=int,
|
174
|
+
default=10,
|
175
|
+
help="Number of logs to send in each batch",
|
176
|
+
)
|
177
|
+
@click.option(
|
178
|
+
"--with-traces",
|
179
|
+
is_flag=True,
|
180
|
+
default=True,
|
181
|
+
help="Generate trace IDs for correlation",
|
182
|
+
)
|
183
|
+
@click.pass_context
|
184
|
+
def generate_command(
|
185
|
+
ctx, count, rate, style, error_rate, services, stream, batch_size, with_traces
|
186
|
+
):
|
187
|
+
"""Generate test logs with optional Burroughs-inspired prose.
|
188
|
+
|
189
|
+
Examples:
|
190
|
+
# Generate 100 test logs
|
191
|
+
foundation logs generate -n 100
|
192
|
+
|
193
|
+
# Generate continuous logs at 5/second
|
194
|
+
foundation logs generate -n 0 -r 5
|
195
|
+
|
196
|
+
# Generate with Burroughs-style messages
|
197
|
+
foundation logs generate --style burroughs
|
198
|
+
|
199
|
+
# Generate with 20% error rate
|
200
|
+
foundation logs generate --error-rate 0.2
|
201
|
+
|
202
|
+
# Generate for specific services
|
203
|
+
foundation logs generate --services "api,auth,payment"
|
204
|
+
"""
|
205
|
+
|
206
|
+
client = ctx.obj.get("client")
|
207
|
+
|
208
|
+
# Parse services
|
209
|
+
if services:
|
210
|
+
service_list = [s.strip() for s in services.split(",")]
|
211
|
+
else:
|
212
|
+
service_list = SERVICES
|
213
|
+
|
214
|
+
click.echo("đ Starting log generation...")
|
215
|
+
click.echo(f" Style: {style}")
|
216
|
+
click.echo(f" Error rate: {error_rate * 100:.0f}%")
|
217
|
+
click.echo(f" Target stream: {stream}")
|
218
|
+
if count == 0:
|
219
|
+
click.echo(f" Mode: Continuous at {rate} logs/second")
|
220
|
+
else:
|
221
|
+
click.echo(f" Count: {count} logs")
|
222
|
+
click.echo(" Press Ctrl+C to stop\n")
|
223
|
+
|
224
|
+
def generate_message(style: str, index: int) -> tuple[str, str]:
|
225
|
+
"""Generate a log message based on style."""
|
226
|
+
if style == "burroughs":
|
227
|
+
message = random.choice(BURROUGHS_PHRASES)
|
228
|
+
level = random.choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"])
|
229
|
+
elif style == "technical":
|
230
|
+
message = random.choice(TECHNICAL_MESSAGES)
|
231
|
+
level = random.choice(["DEBUG", "INFO", "WARN"] * 3 + ["ERROR"])
|
232
|
+
else: # mixed
|
233
|
+
if random.random() > 0.7:
|
234
|
+
message = random.choice(BURROUGHS_PHRASES)
|
235
|
+
level = random.choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"])
|
236
|
+
else:
|
237
|
+
message = random.choice(TECHNICAL_MESSAGES)
|
238
|
+
level = random.choice(["DEBUG", "INFO", "WARN"] * 3 + ["ERROR"])
|
239
|
+
|
240
|
+
# Override level based on error rate
|
241
|
+
if random.random() < error_rate:
|
242
|
+
level = "ERROR"
|
243
|
+
if style != "burroughs":
|
244
|
+
message = f"Error: {message}"
|
245
|
+
|
246
|
+
return message, level
|
247
|
+
|
248
|
+
def generate_log_entry(index: int) -> dict[str, Any]:
|
249
|
+
"""Generate a single log entry."""
|
250
|
+
message, level = generate_message(style, index)
|
251
|
+
service = random.choice(service_list)
|
252
|
+
operation = random.choice(OPERATIONS)
|
253
|
+
|
254
|
+
entry = {
|
255
|
+
"message": f"[{service}] {message}",
|
256
|
+
"level": level,
|
257
|
+
"service": service,
|
258
|
+
"operation": operation,
|
259
|
+
"domain": random.choice(DOMAINS),
|
260
|
+
"action": random.choice(ACTIONS),
|
261
|
+
"status": "degraded" if level == "ERROR" else random.choice(STATUSES),
|
262
|
+
"duration_ms": random.randint(10, 5000),
|
263
|
+
"iteration": index,
|
264
|
+
}
|
265
|
+
|
266
|
+
# Add trace correlation
|
267
|
+
if with_traces:
|
268
|
+
# Group logs by trace (5-10 logs per trace)
|
269
|
+
trace_group = index // random.randint(5, 10)
|
270
|
+
entry["trace_id"] = f"trace_{trace_group:08x}"
|
271
|
+
entry["span_id"] = f"span_{index:08x}"
|
272
|
+
|
273
|
+
# Add error details
|
274
|
+
if level == "ERROR":
|
275
|
+
entry["error_code"] = random.choice([400, 404, 500, 502, 503])
|
276
|
+
entry["error_type"] = random.choice(
|
277
|
+
[
|
278
|
+
"ConnectionTimeout",
|
279
|
+
"ValidationError",
|
280
|
+
"DatabaseError",
|
281
|
+
"ServiceUnavailable",
|
282
|
+
]
|
283
|
+
)
|
284
|
+
|
285
|
+
return entry
|
286
|
+
|
287
|
+
try:
|
288
|
+
logs_sent = 0
|
289
|
+
logs_failed = 0
|
290
|
+
logs_rate_limited = 0
|
291
|
+
start_time = time.time()
|
292
|
+
last_stats_time = start_time
|
293
|
+
last_stats_sent = 0
|
294
|
+
|
295
|
+
# Set up rate limiter
|
296
|
+
# Configure with the target rate and a reasonable burst size
|
297
|
+
rate_limiter = SimpleSyncRateLimiter(
|
298
|
+
capacity=min(
|
299
|
+
rate * 2, 1000
|
300
|
+
), # Allow burst up to 2 seconds worth or 1000
|
301
|
+
refill_rate=rate, # tokens per second
|
302
|
+
)
|
303
|
+
|
304
|
+
# Track rate limiting
|
305
|
+
rate_limit_detected = False
|
306
|
+
consecutive_failures = 0
|
307
|
+
|
308
|
+
def send_log_with_tracking(entry):
|
309
|
+
"""Send log using Foundation's logger and track success/failure."""
|
310
|
+
nonlocal \
|
311
|
+
logs_sent, \
|
312
|
+
logs_failed, \
|
313
|
+
logs_rate_limited, \
|
314
|
+
rate_limit_detected, \
|
315
|
+
consecutive_failures
|
316
|
+
|
317
|
+
try:
|
318
|
+
# Get a logger for the service
|
319
|
+
service_logger = get_logger(f"generated.{entry['service']}")
|
320
|
+
|
321
|
+
# Use Foundation's logger with appropriate level
|
322
|
+
level = entry["level"].lower()
|
323
|
+
message = entry["message"]
|
324
|
+
|
325
|
+
# Remove message and level from attributes since they're passed separately
|
326
|
+
attrs = {
|
327
|
+
k: v for k, v in entry.items() if k not in ["message", "level"]
|
328
|
+
}
|
329
|
+
|
330
|
+
# Call the appropriate log level method
|
331
|
+
if level == "trace":
|
332
|
+
service_logger.trace(message, **attrs)
|
333
|
+
elif level == "debug":
|
334
|
+
service_logger.debug(message, **attrs)
|
335
|
+
elif level == "info":
|
336
|
+
service_logger.info(message, **attrs)
|
337
|
+
elif level == "warn" or level == "warning":
|
338
|
+
service_logger.warning(message, **attrs)
|
339
|
+
elif level == "error":
|
340
|
+
service_logger.error(message, **attrs)
|
341
|
+
elif level == "critical":
|
342
|
+
service_logger.critical(message, **attrs)
|
343
|
+
else:
|
344
|
+
service_logger.info(message, **attrs)
|
345
|
+
|
346
|
+
# Also send to OpenObserve if configured
|
347
|
+
if client:
|
348
|
+
from provide.foundation.observability.openobserve.otlp import (
|
349
|
+
send_log_bulk,
|
350
|
+
)
|
351
|
+
|
352
|
+
success = send_log_bulk(
|
353
|
+
message=message,
|
354
|
+
level=entry["level"],
|
355
|
+
service=entry["service"],
|
356
|
+
attributes=attrs,
|
357
|
+
client=client,
|
358
|
+
)
|
359
|
+
if not success:
|
360
|
+
logs_failed += 1
|
361
|
+
consecutive_failures += 1
|
362
|
+
if consecutive_failures >= 5 and not rate_limit_detected:
|
363
|
+
rate_limit_detected = True
|
364
|
+
logs_rate_limited = logs_failed
|
365
|
+
elif rate_limit_detected:
|
366
|
+
logs_rate_limited += 1
|
367
|
+
return False
|
368
|
+
|
369
|
+
logs_sent += 1
|
370
|
+
consecutive_failures = 0
|
371
|
+
return True
|
372
|
+
|
373
|
+
except Exception as e:
|
374
|
+
log.debug(f"Failed to send log: {e}")
|
375
|
+
logs_failed += 1
|
376
|
+
consecutive_failures += 1
|
377
|
+
return False
|
378
|
+
|
379
|
+
if count == 0:
|
380
|
+
# Continuous mode using Foundation's rate limiter with async workers
|
381
|
+
index = 0
|
382
|
+
import concurrent.futures
|
383
|
+
import queue
|
384
|
+
|
385
|
+
# Work queue for async processing
|
386
|
+
work_queue = queue.Queue(
|
387
|
+
maxsize=int(rate * 2)
|
388
|
+
) # Buffer up to 2 seconds of logs
|
389
|
+
|
390
|
+
# Use thread pool for high-speed sending
|
391
|
+
# More workers for higher rates
|
392
|
+
num_workers = min(50, max(4, int(rate / 100)))
|
393
|
+
|
394
|
+
with concurrent.futures.ThreadPoolExecutor(
|
395
|
+
max_workers=num_workers
|
396
|
+
) as executor:
|
397
|
+
# Start worker threads
|
398
|
+
futures = []
|
399
|
+
|
400
|
+
def worker():
|
401
|
+
"""Worker thread that processes logs from queue."""
|
402
|
+
while True:
|
403
|
+
try:
|
404
|
+
entry = work_queue.get(timeout=1)
|
405
|
+
if entry is None: # Shutdown signal
|
406
|
+
break
|
407
|
+
send_log_with_tracking(entry)
|
408
|
+
work_queue.task_done()
|
409
|
+
except queue.Empty:
|
410
|
+
continue
|
411
|
+
|
412
|
+
# Start workers
|
413
|
+
for _ in range(num_workers):
|
414
|
+
futures.append(executor.submit(worker))
|
415
|
+
|
416
|
+
while True:
|
417
|
+
current_time = time.time()
|
418
|
+
|
419
|
+
# Try to acquire a token from the rate limiter
|
420
|
+
if rate_limiter.acquire():
|
421
|
+
# Token acquired, generate and queue log
|
422
|
+
entry = generate_log_entry(index)
|
423
|
+
try:
|
424
|
+
work_queue.put_nowait(entry)
|
425
|
+
index += 1
|
426
|
+
except queue.Full:
|
427
|
+
# Queue is full, we're generating faster than sending
|
428
|
+
logs_rate_limited += 1
|
429
|
+
if not rate_limit_detected:
|
430
|
+
rate_limit_detected = True
|
431
|
+
log.warning(
|
432
|
+
"â ď¸ Queue full - cannot keep up with target rate"
|
433
|
+
)
|
434
|
+
else:
|
435
|
+
# Rate limited - track it
|
436
|
+
logs_rate_limited += 1
|
437
|
+
if not rate_limit_detected:
|
438
|
+
rate_limit_detected = True
|
439
|
+
log.debug(
|
440
|
+
"â ď¸ Rate limiter activated - target rate exceeded"
|
441
|
+
)
|
442
|
+
|
443
|
+
# Small sleep to prevent busy waiting
|
444
|
+
time.sleep(0.0001)
|
445
|
+
|
446
|
+
# Print stats every second
|
447
|
+
if current_time - last_stats_time >= 1.0:
|
448
|
+
current_sent = logs_sent
|
449
|
+
current_rate = (current_sent - last_stats_sent) / (
|
450
|
+
current_time - last_stats_time
|
451
|
+
)
|
452
|
+
tokens_available = rate_limiter.tokens
|
453
|
+
queue_size = work_queue.qsize()
|
454
|
+
|
455
|
+
status = f"đ Sent: {logs_sent:,} | Rate: {current_rate:.0f}/s | Tokens: {tokens_available:.0f}/{rate_limiter.capacity} | Queue: {queue_size}"
|
456
|
+
if logs_failed > 0:
|
457
|
+
status += f" | Failed: {logs_failed:,}"
|
458
|
+
if rate_limit_detected:
|
459
|
+
status += f" | â ď¸ RATE LIMITED ({logs_rate_limited:,} throttled)"
|
460
|
+
|
461
|
+
click.echo(status)
|
462
|
+
last_stats_time = current_time
|
463
|
+
last_stats_sent = current_sent
|
464
|
+
|
465
|
+
else:
|
466
|
+
# Fixed count mode using Foundation's rate limiter with async workers
|
467
|
+
import concurrent.futures
|
468
|
+
import queue
|
469
|
+
|
470
|
+
# Work queue for async processing
|
471
|
+
work_queue = queue.Queue(maxsize=min(1000, count))
|
472
|
+
|
473
|
+
# Use thread pool for high-speed sending
|
474
|
+
num_workers = min(50, max(4, int(rate / 100)))
|
475
|
+
|
476
|
+
with concurrent.futures.ThreadPoolExecutor(
|
477
|
+
max_workers=num_workers
|
478
|
+
) as executor:
|
479
|
+
# Start worker threads
|
480
|
+
def worker():
|
481
|
+
"""Worker thread that processes logs from queue."""
|
482
|
+
while True:
|
483
|
+
try:
|
484
|
+
entry = work_queue.get(timeout=1)
|
485
|
+
if entry is None: # Shutdown signal
|
486
|
+
break
|
487
|
+
send_log_with_tracking(entry)
|
488
|
+
work_queue.task_done()
|
489
|
+
except queue.Empty:
|
490
|
+
continue
|
491
|
+
|
492
|
+
# Start workers
|
493
|
+
workers = [executor.submit(worker) for _ in range(num_workers)]
|
494
|
+
|
495
|
+
# Generate and queue logs
|
496
|
+
for i in range(count):
|
497
|
+
# Wait for rate limiter token
|
498
|
+
while not rate_limiter.acquire():
|
499
|
+
logs_rate_limited += 1
|
500
|
+
if not rate_limit_detected:
|
501
|
+
rate_limit_detected = True
|
502
|
+
log.debug(
|
503
|
+
"â ď¸ Rate limiter activated - target rate exceeded"
|
504
|
+
)
|
505
|
+
time.sleep(
|
506
|
+
0.0001
|
507
|
+
) # Very small sleep to prevent busy waiting
|
508
|
+
|
509
|
+
# Generate and queue log
|
510
|
+
entry = generate_log_entry(i)
|
511
|
+
work_queue.put(entry)
|
512
|
+
|
513
|
+
# Print progress
|
514
|
+
if (i + 1) % 100 == 0:
|
515
|
+
current_time = time.time()
|
516
|
+
elapsed = current_time - start_time
|
517
|
+
current_rate = logs_sent / elapsed if elapsed > 0 else 0
|
518
|
+
tokens_available = rate_limiter.tokens
|
519
|
+
queue_size = work_queue.qsize()
|
520
|
+
|
521
|
+
status = f"đ Progress: {i + 1}/{count} | Sent: {logs_sent:,} | Rate: {current_rate:.0f}/s | Queue: {queue_size} | Tokens: {tokens_available:.0f}"
|
522
|
+
if logs_failed > 0:
|
523
|
+
status += f" | Failed: {logs_failed}"
|
524
|
+
if rate_limit_detected:
|
525
|
+
status += f" | â ď¸ THROTTLED ({logs_rate_limited:,})"
|
526
|
+
|
527
|
+
click.echo(status)
|
528
|
+
|
529
|
+
# Wait for queue to empty
|
530
|
+
click.echo("âł Waiting for queue to empty...")
|
531
|
+
work_queue.join()
|
532
|
+
|
533
|
+
# Shutdown workers
|
534
|
+
for _ in range(num_workers):
|
535
|
+
work_queue.put(None)
|
536
|
+
|
537
|
+
# Wait for workers to finish
|
538
|
+
concurrent.futures.wait(workers)
|
539
|
+
|
540
|
+
elapsed = time.time() - start_time
|
541
|
+
rate_actual = logs_sent / elapsed if elapsed > 0 else 0
|
542
|
+
|
543
|
+
click.echo("\nđ Generation complete:")
|
544
|
+
click.echo(f" Total sent: {logs_sent:,} logs")
|
545
|
+
click.echo(f" Total failed: {logs_failed:,} logs")
|
546
|
+
if rate_limit_detected:
|
547
|
+
click.echo(f" â ď¸ Rate limited: {logs_rate_limited:,} logs")
|
548
|
+
click.echo(f" Time: {elapsed:.2f}s")
|
549
|
+
click.echo(f" Target rate: {rate:.0f} logs/second")
|
550
|
+
click.echo(f" Actual rate: {rate_actual:.1f} logs/second")
|
551
|
+
if rate_limit_detected and rate_actual < rate * 0.5:
|
552
|
+
click.echo(
|
553
|
+
f" â ď¸ Rate limiting detected - actual rate is {(rate_actual / rate) * 100:.0f}% of target"
|
554
|
+
)
|
555
|
+
|
556
|
+
except KeyboardInterrupt:
|
557
|
+
click.echo(f"\nâ Stopped. Generated {logs_sent} logs.")
|
558
|
+
except Exception as e:
|
559
|
+
click.echo(f"Generation failed: {e}", err=True)
|
560
|
+
return 1
|
561
|
+
|
562
|
+
else:
|
563
|
+
|
564
|
+
def generate_command(*args, **kwargs):
|
565
|
+
"""Generate command stub when click is not available."""
|
566
|
+
raise ImportError(
|
567
|
+
"CLI commands require optional dependencies. "
|
568
|
+
"Install with: pip install 'provide-foundation[cli]'"
|
569
|
+
)
|