solnlib 8.1.0__tar.gz → 8.1.0b2__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.
- {solnlib-8.1.0 → solnlib-8.1.0b2}/PKG-INFO +1 -1
- {solnlib-8.1.0 → solnlib-8.1.0b2}/pyproject.toml +1 -1
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/__init__.py +1 -1
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/observability.py +287 -26
- {solnlib-8.1.0 → solnlib-8.1.0b2}/LICENSE +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/_settings.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/_utils.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/acl.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/alerts_rest_client.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/bulletin_rest_client.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/concurrent/concurrent_executor.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/concurrent/process_pool.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/concurrent/thread_pool.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/conf_manager.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/credentials.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/file_monitor.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/hec_config.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/log.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/__init__.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/checkpointer.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/event.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/event_writer.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/modinput.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/modular_input/modular_input.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/net_utils.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/orphan_process_monitor.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/pattern.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/rest.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/schedule/job.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/schedule/scheduler.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/server_info.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/soln_exceptions.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/splunk_rest_client.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/splunkenv.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/time_parser.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/timer_queue.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/user_access.py +0 -0
- {solnlib-8.1.0 → solnlib-8.1.0b2}/solnlib/utils.py +0 -0
|
@@ -73,6 +73,73 @@ _SERVICE_NAMESPACE = "splunk.addon"
|
|
|
73
73
|
ATTR_MODINPUT_NAME = "splunk.modinput.name"
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _sanitize_for_log(text) -> str:
|
|
77
|
+
"""Return one bounded, UTF-8-safe physical log-line fragment."""
|
|
78
|
+
try:
|
|
79
|
+
# Calling the base implementation directly neutralizes overridden
|
|
80
|
+
# methods on a str subclass and returns a genuine plain str.
|
|
81
|
+
text = str.__str__(text) if isinstance(text, str) else str(text)
|
|
82
|
+
text = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
83
|
+
text = text.encode("utf-8", errors="replace").decode("utf-8")
|
|
84
|
+
if len(text) > 500:
|
|
85
|
+
text = text[:500] + "...(truncated)"
|
|
86
|
+
return text
|
|
87
|
+
except BaseException:
|
|
88
|
+
return "<unrepresentable>"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _safe_exception_repr(error: BaseException) -> str:
|
|
92
|
+
try:
|
|
93
|
+
return _sanitize_for_log(repr(error))
|
|
94
|
+
except BaseException:
|
|
95
|
+
pass
|
|
96
|
+
try:
|
|
97
|
+
return _sanitize_for_log(f"{type(error).__name__} (repr unavailable)")
|
|
98
|
+
except BaseException:
|
|
99
|
+
return "<exception repr unavailable>"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _safe_exception_str(error: BaseException) -> str:
|
|
103
|
+
try:
|
|
104
|
+
return _sanitize_for_log(str(error))
|
|
105
|
+
except BaseException:
|
|
106
|
+
pass
|
|
107
|
+
try:
|
|
108
|
+
return _sanitize_for_log(f"{type(error).__name__} (details unavailable)")
|
|
109
|
+
except BaseException:
|
|
110
|
+
return "<exception details unavailable>"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _DowngradeToInfoFilter(logging.Filter):
|
|
114
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
115
|
+
if record.levelno > logging.INFO:
|
|
116
|
+
record.levelno = logging.INFO
|
|
117
|
+
record.levelname = "INFO"
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_downgrade_to_info_filter = _DowngradeToInfoFilter()
|
|
122
|
+
|
|
123
|
+
# Intentional process-wide policy: observability is auxiliary functionality, so
|
|
124
|
+
# diagnostics emitted by the OpenTelemetry SDK and exporters are capped at INFO
|
|
125
|
+
# across all signal types (metrics, logs, and traces). This preserves diagnostic
|
|
126
|
+
# messages without surfacing observability failures as add-on WARNING/ERROR events.
|
|
127
|
+
_OTLP_LOGGERS = (
|
|
128
|
+
"opentelemetry.exporter.otlp.proto.grpc.exporter",
|
|
129
|
+
"opentelemetry.util.re",
|
|
130
|
+
"opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder",
|
|
131
|
+
"opentelemetry.exporter.otlp.proto.common._internal",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
_METRICS_SDK_LOGGERS = (
|
|
135
|
+
"opentelemetry.sdk.metrics._internal.export",
|
|
136
|
+
"opentelemetry.sdk.metrics._internal",
|
|
137
|
+
"opentelemetry.sdk.metrics._internal.instrument",
|
|
138
|
+
"opentelemetry.metrics._internal",
|
|
139
|
+
"opentelemetry.attributes",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
76
143
|
class LoggerMetricExporter(MetricExporter):
|
|
77
144
|
"""An OpenTelemetry ``MetricExporter`` that logs every data point.
|
|
78
145
|
|
|
@@ -166,8 +233,10 @@ class LoggerMetricExporter(MetricExporter):
|
|
|
166
233
|
metric_count,
|
|
167
234
|
)
|
|
168
235
|
return MetricExportResult.SUCCESS
|
|
169
|
-
except Exception as
|
|
170
|
-
self._logger.
|
|
236
|
+
except Exception as error:
|
|
237
|
+
self._logger.info(
|
|
238
|
+
"Failed to export metrics: %s", _safe_exception_str(error)
|
|
239
|
+
)
|
|
171
240
|
return MetricExportResult.FAILURE
|
|
172
241
|
|
|
173
242
|
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
|
|
@@ -179,12 +248,79 @@ class LoggerMetricExporter(MetricExporter):
|
|
|
179
248
|
return True
|
|
180
249
|
|
|
181
250
|
|
|
251
|
+
class _CircuitBreakerExporter(MetricExporter):
|
|
252
|
+
"""Wraps the internally constructed OTLP exporter and stops calling it
|
|
253
|
+
after 3 consecutive failed exports in this process."""
|
|
254
|
+
|
|
255
|
+
_MAX_CONSECUTIVE_FAILURES = 3
|
|
256
|
+
|
|
257
|
+
def __init__(self, inner: MetricExporter, logger: _Logger) -> None:
|
|
258
|
+
super().__init__(
|
|
259
|
+
preferred_temporality=inner._preferred_temporality,
|
|
260
|
+
preferred_aggregation=inner._preferred_aggregation,
|
|
261
|
+
)
|
|
262
|
+
self._inner = inner
|
|
263
|
+
self._logger = logger
|
|
264
|
+
self._consecutive_failures = 0
|
|
265
|
+
self._tripped = False
|
|
266
|
+
self._shutdown_called = False
|
|
267
|
+
self._lock = threading.Lock()
|
|
268
|
+
|
|
269
|
+
def export(
|
|
270
|
+
self,
|
|
271
|
+
metrics_data: MetricsData,
|
|
272
|
+
timeout_millis: float = 10_000,
|
|
273
|
+
**kwargs,
|
|
274
|
+
) -> MetricExportResult:
|
|
275
|
+
with self._lock:
|
|
276
|
+
if self._tripped:
|
|
277
|
+
return MetricExportResult.SUCCESS
|
|
278
|
+
|
|
279
|
+
# Never hold _lock across this blocking call: shutdown() must be able
|
|
280
|
+
# to reach the inner exporter and interrupt an in-flight retry even
|
|
281
|
+
# while an export is still outstanding on another thread.
|
|
282
|
+
try:
|
|
283
|
+
result = self._inner.export(
|
|
284
|
+
metrics_data, timeout_millis=timeout_millis, **kwargs
|
|
285
|
+
)
|
|
286
|
+
except Exception as error:
|
|
287
|
+
self._logger.info(
|
|
288
|
+
"OTLP export raised an exception: %s",
|
|
289
|
+
_safe_exception_repr(error),
|
|
290
|
+
)
|
|
291
|
+
result = MetricExportResult.FAILURE
|
|
292
|
+
|
|
293
|
+
with self._lock:
|
|
294
|
+
if result == MetricExportResult.SUCCESS:
|
|
295
|
+
self._consecutive_failures = 0
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
self._consecutive_failures += 1
|
|
299
|
+
if self._consecutive_failures >= self._MAX_CONSECUTIVE_FAILURES:
|
|
300
|
+
self._tripped = True
|
|
301
|
+
self._logger.info("OTLP export disabled after 3 consecutive failures")
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
def force_flush(self, timeout_millis: float = 10_000) -> bool:
|
|
305
|
+
with self._lock:
|
|
306
|
+
if self._tripped:
|
|
307
|
+
return True
|
|
308
|
+
return self._inner.force_flush(timeout_millis=timeout_millis)
|
|
309
|
+
|
|
310
|
+
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
|
|
311
|
+
with self._lock:
|
|
312
|
+
if self._shutdown_called:
|
|
313
|
+
return
|
|
314
|
+
self._shutdown_called = True
|
|
315
|
+
self._inner.shutdown(timeout_millis=timeout_millis, **kwargs)
|
|
316
|
+
|
|
317
|
+
|
|
182
318
|
class ObservabilityService:
|
|
183
319
|
"""OpenTelemetry observability service for a Splunk modular input.
|
|
184
320
|
|
|
185
321
|
Sets up a ``MeterProvider`` with two built-in event counters and,
|
|
186
322
|
when the Spotlight collector is reachable, an OTLP gRPC exporter.
|
|
187
|
-
Initialisation failures are caught and logged
|
|
323
|
+
Initialisation failures are caught and logged at INFO so that a
|
|
188
324
|
missing or misconfigured observability stack never breaks the add-on.
|
|
189
325
|
|
|
190
326
|
**Resource attributes** (fixed for the lifetime of the process):
|
|
@@ -275,7 +411,18 @@ class ObservabilityService:
|
|
|
275
411
|
self._meter: Optional[Meter] = None
|
|
276
412
|
self._provider: Optional[MeterProvider] = None
|
|
277
413
|
|
|
414
|
+
for logger_name in _METRICS_SDK_LOGGERS:
|
|
415
|
+
logging.getLogger(logger_name).addFilter(_downgrade_to_info_filter)
|
|
416
|
+
|
|
278
417
|
try:
|
|
418
|
+
if type(modinput_type) is not str or not _is_safe_identifier_str(
|
|
419
|
+
modinput_type
|
|
420
|
+
):
|
|
421
|
+
raise ValueError(
|
|
422
|
+
"modinput_type must be a str without CR/LF, got "
|
|
423
|
+
f"{_sanitize_for_log(type(modinput_type).__name__)}"
|
|
424
|
+
)
|
|
425
|
+
|
|
279
426
|
if ta_name is None or ta_version is None:
|
|
280
427
|
_ta_name, _ta_version = self._read_ta_info()
|
|
281
428
|
ta_name = ta_name or _ta_name
|
|
@@ -327,8 +474,11 @@ class ObservabilityService:
|
|
|
327
474
|
ta_version,
|
|
328
475
|
modinput_type,
|
|
329
476
|
)
|
|
330
|
-
except Exception as
|
|
331
|
-
self._logger.
|
|
477
|
+
except Exception as error:
|
|
478
|
+
self._logger.info(
|
|
479
|
+
"Failed to initialise ObservabilityService: %s",
|
|
480
|
+
_safe_exception_str(error),
|
|
481
|
+
)
|
|
332
482
|
|
|
333
483
|
def _read_ta_info(self) -> tuple[Optional[str], Optional[str]]:
|
|
334
484
|
"""Read the add-on name and version from ``app.conf``.
|
|
@@ -345,8 +495,11 @@ class ObservabilityService:
|
|
|
345
495
|
)
|
|
346
496
|
ta_version = scoped_stanzas.get("launcher", {}).get("version") or None
|
|
347
497
|
return ta_name, ta_version
|
|
348
|
-
except Exception as
|
|
349
|
-
self._logger.
|
|
498
|
+
except Exception as error:
|
|
499
|
+
self._logger.info(
|
|
500
|
+
"Failed to read TA info from app.conf: %s",
|
|
501
|
+
_safe_exception_str(error),
|
|
502
|
+
)
|
|
350
503
|
return None, None
|
|
351
504
|
|
|
352
505
|
def _get_ipc_broker_port(self) -> Optional[int]:
|
|
@@ -359,9 +512,10 @@ class ObservabilityService:
|
|
|
359
512
|
try:
|
|
360
513
|
stanzas = get_conf_stanzas("server")
|
|
361
514
|
return int(stanzas["ipc_broker"]["port"])
|
|
362
|
-
except Exception as
|
|
363
|
-
self._logger.
|
|
364
|
-
"Failed to read IPC broker port from server.conf: %s",
|
|
515
|
+
except Exception as error:
|
|
516
|
+
self._logger.info(
|
|
517
|
+
"Failed to read IPC broker port from server.conf: %s",
|
|
518
|
+
_safe_exception_str(error),
|
|
365
519
|
)
|
|
366
520
|
return None
|
|
367
521
|
|
|
@@ -376,7 +530,7 @@ class ObservabilityService:
|
|
|
376
530
|
"""
|
|
377
531
|
ipc_broker_port = self._get_ipc_broker_port()
|
|
378
532
|
if ipc_broker_port is None:
|
|
379
|
-
self._logger.
|
|
533
|
+
self._logger.info("IPC broker port not found in server.conf")
|
|
380
534
|
return None
|
|
381
535
|
|
|
382
536
|
url = (
|
|
@@ -397,15 +551,18 @@ class ObservabilityService:
|
|
|
397
551
|
data = json.loads(resp.read().decode())
|
|
398
552
|
|
|
399
553
|
if not data.get("success"):
|
|
400
|
-
self._logger.
|
|
554
|
+
self._logger.info(
|
|
401
555
|
"IPC broker discovery returned unsuccessful response: %s", data
|
|
402
556
|
)
|
|
403
557
|
return None
|
|
404
558
|
port = str(data["port"])
|
|
405
559
|
self._logger.info("Discovered OTLP port via IPC broker: %s", port)
|
|
406
560
|
return port
|
|
407
|
-
except Exception as
|
|
408
|
-
self._logger.
|
|
561
|
+
except Exception as error:
|
|
562
|
+
self._logger.info(
|
|
563
|
+
"IPC broker OTLP port discovery failed: %s",
|
|
564
|
+
_safe_exception_str(error),
|
|
565
|
+
)
|
|
409
566
|
return None
|
|
410
567
|
|
|
411
568
|
def _resolve_otlp_port(self) -> Optional[str]:
|
|
@@ -443,13 +600,16 @@ class ObservabilityService:
|
|
|
443
600
|
``AggregationTemporality.DELTA`` so that each export interval reports
|
|
444
601
|
only the change since the previous interval.
|
|
445
602
|
|
|
446
|
-
Returns the configured exporter
|
|
603
|
+
Returns the configured exporter wrapped in ``_CircuitBreakerExporter``,
|
|
604
|
+
or ``None`` when:
|
|
447
605
|
|
|
448
606
|
- The OTLP port cannot be resolved (see :meth:`_resolve_otlp_port`).
|
|
449
607
|
- The certificate file does not exist.
|
|
450
608
|
- Any other exception occurs during exporter construction (including a
|
|
451
609
|
missing ``grpcio`` package).
|
|
452
610
|
"""
|
|
611
|
+
os.environ.setdefault("GRPC_VERBOSITY", "NONE")
|
|
612
|
+
|
|
453
613
|
try:
|
|
454
614
|
import grpc
|
|
455
615
|
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
@@ -466,7 +626,7 @@ class ObservabilityService:
|
|
|
466
626
|
)
|
|
467
627
|
|
|
468
628
|
if not otel_port:
|
|
469
|
-
self._logger.
|
|
629
|
+
self._logger.info(
|
|
470
630
|
"OTLP port could not be determined from env or IPC broker, "
|
|
471
631
|
"OTLP export disabled"
|
|
472
632
|
)
|
|
@@ -481,12 +641,15 @@ class ObservabilityService:
|
|
|
481
641
|
)
|
|
482
642
|
|
|
483
643
|
if not os.path.exists(cert_file):
|
|
484
|
-
self._logger.
|
|
644
|
+
self._logger.info(
|
|
485
645
|
"OTel Collector certificate not found at %s, OTLP export disabled",
|
|
486
646
|
cert_file,
|
|
487
647
|
)
|
|
488
648
|
return None
|
|
489
649
|
|
|
650
|
+
for logger_name in _OTLP_LOGGERS:
|
|
651
|
+
logging.getLogger(logger_name).addFilter(_downgrade_to_info_filter)
|
|
652
|
+
|
|
490
653
|
with open(cert_file, "rb") as f:
|
|
491
654
|
server_cert = f.read()
|
|
492
655
|
|
|
@@ -500,11 +663,12 @@ class ObservabilityService:
|
|
|
500
663
|
},
|
|
501
664
|
)
|
|
502
665
|
self._logger.info("OTLP gRPC exporter configured with TLS for %s", endpoint)
|
|
503
|
-
return exporter
|
|
666
|
+
return _CircuitBreakerExporter(exporter, self._logger)
|
|
504
667
|
|
|
505
|
-
except Exception as
|
|
506
|
-
self._logger.
|
|
507
|
-
"Failed to configure OTLP exporter: %s",
|
|
668
|
+
except Exception as error:
|
|
669
|
+
self._logger.info(
|
|
670
|
+
"Failed to configure OTLP exporter: %s",
|
|
671
|
+
_safe_exception_str(error),
|
|
508
672
|
)
|
|
509
673
|
return None
|
|
510
674
|
|
|
@@ -565,8 +729,65 @@ class ObservabilityService:
|
|
|
565
729
|
return
|
|
566
730
|
try:
|
|
567
731
|
self._provider.force_flush(timeout_millis=int(timeout_millis))
|
|
568
|
-
except Exception as
|
|
569
|
-
self._logger.
|
|
732
|
+
except Exception as error:
|
|
733
|
+
self._logger.info("Failed to flush metrics: %s", _safe_exception_str(error))
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
_INT64_MIN = -(2**63)
|
|
737
|
+
_INT64_MAX = 2**63 - 1
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _is_encodable_str(value: str) -> bool:
|
|
741
|
+
# Caller guarantees type(value) is str.
|
|
742
|
+
try:
|
|
743
|
+
value.encode("utf-8")
|
|
744
|
+
return True
|
|
745
|
+
except UnicodeEncodeError:
|
|
746
|
+
return False
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _is_safe_identifier_str(value: str) -> bool:
|
|
750
|
+
return _is_encodable_str(value) and "\n" not in value and "\r" not in value
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _count_error(value) -> Optional[str]:
|
|
754
|
+
"""Return None for a valid event/byte count, else a safe reason string."""
|
|
755
|
+
if type(value) is not int:
|
|
756
|
+
return f"expected int, got {_sanitize_for_log(type(value).__name__)}"
|
|
757
|
+
if value < 0:
|
|
758
|
+
return "count is negative"
|
|
759
|
+
if value > _INT64_MAX:
|
|
760
|
+
return f"count exceeds int64 range ({value.bit_length()} bits)"
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _attr_key_error(key) -> Optional[str]:
|
|
765
|
+
"""Return None for a valid attribute key, else a safe reason string."""
|
|
766
|
+
if type(key) is not str:
|
|
767
|
+
return f"expected str key, got {_sanitize_for_log(type(key).__name__)}"
|
|
768
|
+
if not key:
|
|
769
|
+
return "key is empty"
|
|
770
|
+
if not _is_safe_identifier_str(key):
|
|
771
|
+
return "key is not UTF-8 encodable or contains CR/LF"
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _attr_value_error(value) -> Optional[str]:
|
|
776
|
+
"""Return None for a valid attribute value, else a safe reason string."""
|
|
777
|
+
value_type = type(value)
|
|
778
|
+
if value_type is bool:
|
|
779
|
+
return None
|
|
780
|
+
if value_type is int:
|
|
781
|
+
if _INT64_MIN <= value <= _INT64_MAX:
|
|
782
|
+
return None
|
|
783
|
+
return f"int value out of int64 range ({value.bit_length()} bits)"
|
|
784
|
+
if value_type is float:
|
|
785
|
+
return None
|
|
786
|
+
if value_type is str:
|
|
787
|
+
if _is_encodable_str(value):
|
|
788
|
+
return None
|
|
789
|
+
return "value is not UTF-8 encodable"
|
|
790
|
+
return f"unsupported value type: {_sanitize_for_log(value_type.__name__)}"
|
|
570
791
|
|
|
571
792
|
|
|
572
793
|
class StanzaObservabilityRecorder:
|
|
@@ -654,6 +875,17 @@ class StanzaObservabilityRecorder:
|
|
|
654
875
|
``"my_stanza"``). Attached as ``"splunk.modinput.name"``
|
|
655
876
|
on every recorded data point.
|
|
656
877
|
"""
|
|
878
|
+
self._logger = logger
|
|
879
|
+
if type(modinput_type) is not str or not _is_safe_identifier_str(modinput_type):
|
|
880
|
+
raise TypeError(
|
|
881
|
+
"modinput_type must be a str without CR/LF, got "
|
|
882
|
+
f"{_sanitize_for_log(type(modinput_type).__name__)}"
|
|
883
|
+
)
|
|
884
|
+
if type(stanza_name) is not str or not _is_safe_identifier_str(stanza_name):
|
|
885
|
+
raise TypeError(
|
|
886
|
+
"stanza_name must be a str without CR/LF, got "
|
|
887
|
+
f"{_sanitize_for_log(type(stanza_name).__name__)}"
|
|
888
|
+
)
|
|
657
889
|
self._stanza_name = stanza_name
|
|
658
890
|
self._service = self._get_or_create_service(modinput_type, logger)
|
|
659
891
|
self._emit_zero_baseline()
|
|
@@ -744,11 +976,40 @@ class StanzaObservabilityRecorder:
|
|
|
744
976
|
extra_attrs={"my_ta.partition": partition_id},
|
|
745
977
|
)
|
|
746
978
|
"""
|
|
747
|
-
attrs =
|
|
979
|
+
attrs = {}
|
|
980
|
+
if extra_attrs is not None:
|
|
981
|
+
if type(extra_attrs) is not dict:
|
|
982
|
+
self._logger.info(
|
|
983
|
+
"Ignoring extra_attrs: expected dict or None, got %s",
|
|
984
|
+
_sanitize_for_log(type(extra_attrs).__name__),
|
|
985
|
+
)
|
|
986
|
+
else:
|
|
987
|
+
for key, value in extra_attrs.items():
|
|
988
|
+
key_error = _attr_key_error(key)
|
|
989
|
+
if key_error is not None:
|
|
990
|
+
self._logger.info("Ignoring invalid attribute: %s", key_error)
|
|
991
|
+
continue
|
|
992
|
+
value_error = _attr_value_error(value)
|
|
993
|
+
if value_error is not None:
|
|
994
|
+
self._logger.info(
|
|
995
|
+
"Ignoring invalid value for attribute %r: %s",
|
|
996
|
+
key,
|
|
997
|
+
value_error,
|
|
998
|
+
)
|
|
999
|
+
continue
|
|
1000
|
+
attrs[key] = value
|
|
748
1001
|
attrs[ATTR_MODINPUT_NAME] = self._stanza_name
|
|
749
|
-
|
|
1002
|
+
|
|
1003
|
+
event_count_error = _count_error(event_count)
|
|
1004
|
+
if event_count_error is not None:
|
|
1005
|
+
self._logger.info("Skipping invalid event_count: %s", event_count_error)
|
|
1006
|
+
elif self._service.event_count_counter:
|
|
750
1007
|
self._service.event_count_counter.add(event_count, attributes=attrs)
|
|
751
|
-
|
|
1008
|
+
|
|
1009
|
+
byte_count_error = _count_error(byte_count)
|
|
1010
|
+
if byte_count_error is not None:
|
|
1011
|
+
self._logger.info("Skipping invalid byte_count: %s", byte_count_error)
|
|
1012
|
+
elif self._service.event_bytes_counter:
|
|
752
1013
|
self._service.event_bytes_counter.add(byte_count, attributes=attrs)
|
|
753
1014
|
|
|
754
1015
|
def flush(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|