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,134 @@
|
|
1
|
+
"""
|
2
|
+
Data models for OpenObserve API requests and responses.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class SearchQuery:
|
12
|
+
"""Search query parameters for OpenObserve."""
|
13
|
+
|
14
|
+
sql: str
|
15
|
+
start_time: int # Microseconds since epoch
|
16
|
+
end_time: int # Microseconds since epoch
|
17
|
+
from_offset: int = 0
|
18
|
+
size: int = 100
|
19
|
+
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
21
|
+
"""Convert to API request format."""
|
22
|
+
return {
|
23
|
+
"query": {
|
24
|
+
"sql": self.sql,
|
25
|
+
"start_time": self.start_time,
|
26
|
+
"end_time": self.end_time,
|
27
|
+
"from": self.from_offset,
|
28
|
+
"size": self.size,
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class SearchResponse:
|
35
|
+
"""Response from OpenObserve search API."""
|
36
|
+
|
37
|
+
hits: list[dict[str, Any]]
|
38
|
+
total: int
|
39
|
+
took: int # Milliseconds
|
40
|
+
scan_size: int
|
41
|
+
trace_id: str | None = None
|
42
|
+
from_offset: int = 0
|
43
|
+
size: int = 0
|
44
|
+
is_partial: bool = False
|
45
|
+
function_error: list[str] = field(default_factory=list)
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def from_dict(cls, data: dict[str, Any]) -> "SearchResponse":
|
49
|
+
"""Create from API response."""
|
50
|
+
return cls(
|
51
|
+
hits=data.get("hits", []),
|
52
|
+
total=data.get("total", 0),
|
53
|
+
took=data.get("took", 0),
|
54
|
+
scan_size=data.get("scan_size", 0),
|
55
|
+
trace_id=data.get("trace_id"),
|
56
|
+
from_offset=data.get("from", 0),
|
57
|
+
size=data.get("size", 0),
|
58
|
+
is_partial=data.get("is_partial", False),
|
59
|
+
function_error=data.get("function_error", []),
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class StreamInfo:
|
65
|
+
"""Information about an OpenObserve stream."""
|
66
|
+
|
67
|
+
name: str
|
68
|
+
storage_type: str
|
69
|
+
stream_type: str
|
70
|
+
doc_count: int = 0
|
71
|
+
compressed_size: int = 0
|
72
|
+
original_size: int = 0
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def from_dict(cls, data: dict[str, Any]) -> "StreamInfo":
|
76
|
+
"""Create from API response."""
|
77
|
+
return cls(
|
78
|
+
name=data.get("name", ""),
|
79
|
+
storage_type=data.get("storage_type", ""),
|
80
|
+
stream_type=data.get("stream_type", ""),
|
81
|
+
doc_count=data.get("stats", {}).get("doc_count", 0),
|
82
|
+
compressed_size=data.get("stats", {}).get("compressed_size", 0),
|
83
|
+
original_size=data.get("stats", {}).get("original_size", 0),
|
84
|
+
)
|
85
|
+
|
86
|
+
|
87
|
+
def parse_relative_time(time_str: str, now: datetime | None = None) -> int:
|
88
|
+
"""Parse relative time strings like '-1h', '-30m' to microseconds since epoch.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
time_str: Time string (e.g., '-1h', '-30m', 'now')
|
92
|
+
now: Current time (for testing), defaults to datetime.now()
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
Microseconds since epoch
|
96
|
+
"""
|
97
|
+
from datetime import timedelta
|
98
|
+
|
99
|
+
if now is None:
|
100
|
+
now = datetime.now()
|
101
|
+
|
102
|
+
if time_str == "now":
|
103
|
+
return int(now.timestamp() * 1_000_000)
|
104
|
+
|
105
|
+
if time_str.startswith("-"):
|
106
|
+
# Parse relative time
|
107
|
+
value = time_str[1:]
|
108
|
+
if value.endswith("h"):
|
109
|
+
delta = timedelta(hours=int(value[:-1]))
|
110
|
+
elif value.endswith("m"):
|
111
|
+
delta = timedelta(minutes=int(value[:-1]))
|
112
|
+
elif value.endswith("s"):
|
113
|
+
delta = timedelta(seconds=int(value[:-1]))
|
114
|
+
elif value.endswith("d"):
|
115
|
+
delta = timedelta(days=int(value[:-1]))
|
116
|
+
else:
|
117
|
+
# Assume seconds if no unit
|
118
|
+
delta = timedelta(seconds=int(value))
|
119
|
+
|
120
|
+
target_time = now - delta
|
121
|
+
return int(target_time.timestamp() * 1_000_000)
|
122
|
+
|
123
|
+
# Try to parse as timestamp
|
124
|
+
try:
|
125
|
+
timestamp = int(time_str)
|
126
|
+
# If it's already in microseconds (large number), return as-is
|
127
|
+
if timestamp > 1_000_000_000_000:
|
128
|
+
return timestamp
|
129
|
+
# Otherwise assume seconds and convert
|
130
|
+
return timestamp * 1_000_000
|
131
|
+
except ValueError:
|
132
|
+
# Try to parse as ISO datetime
|
133
|
+
dt = datetime.fromisoformat(time_str)
|
134
|
+
return int(dt.timestamp() * 1_000_000)
|
@@ -0,0 +1,320 @@
|
|
1
|
+
"""
|
2
|
+
OTLP integration for sending logs to OpenObserve.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
import json
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from provide.foundation.logger import get_logger
|
10
|
+
from provide.foundation.observability.openobserve.client import OpenObserveClient
|
11
|
+
|
12
|
+
log = get_logger(__name__)
|
13
|
+
|
14
|
+
# OpenTelemetry feature detection
|
15
|
+
try:
|
16
|
+
from opentelemetry import trace
|
17
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
18
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
19
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
20
|
+
from opentelemetry.sdk.resources import Resource
|
21
|
+
from opentelemetry.semconv.resource import ResourceAttributes
|
22
|
+
|
23
|
+
_HAS_OTEL_LOGS = True
|
24
|
+
except ImportError:
|
25
|
+
_HAS_OTEL_LOGS = False
|
26
|
+
|
27
|
+
|
28
|
+
def send_log_otlp(
|
29
|
+
message: str,
|
30
|
+
level: str = "INFO",
|
31
|
+
service: str | None = None,
|
32
|
+
attributes: dict[str, Any] | None = None,
|
33
|
+
) -> bool:
|
34
|
+
"""Send a log via OTLP if available.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
message: Log message
|
38
|
+
level: Log level
|
39
|
+
service: Service name (uses config if not provided)
|
40
|
+
attributes: Additional attributes
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
True if sent successfully via OTLP, False otherwise
|
44
|
+
"""
|
45
|
+
if not _HAS_OTEL_LOGS:
|
46
|
+
return False
|
47
|
+
|
48
|
+
try:
|
49
|
+
from provide.foundation.logger.config import TelemetryConfig
|
50
|
+
|
51
|
+
config = TelemetryConfig.from_env()
|
52
|
+
|
53
|
+
if not config.otlp_endpoint:
|
54
|
+
return False
|
55
|
+
|
56
|
+
# Create resource with service info
|
57
|
+
resource_attrs = {
|
58
|
+
ResourceAttributes.SERVICE_NAME: service
|
59
|
+
or config.service_name
|
60
|
+
or "foundation",
|
61
|
+
}
|
62
|
+
if config.service_version:
|
63
|
+
resource_attrs[ResourceAttributes.SERVICE_VERSION] = config.service_version
|
64
|
+
|
65
|
+
resource = Resource.create(resource_attrs)
|
66
|
+
|
67
|
+
# Configure OTLP exporter
|
68
|
+
headers = config.get_otlp_headers_dict()
|
69
|
+
if config.openobserve_org:
|
70
|
+
# Add organization header for OpenObserve
|
71
|
+
headers["organization"] = config.openobserve_org
|
72
|
+
headers["stream-name"] = config.openobserve_stream
|
73
|
+
|
74
|
+
# Determine endpoint for logs
|
75
|
+
if config.otlp_traces_endpoint:
|
76
|
+
# Replace /traces with /logs
|
77
|
+
logs_endpoint = config.otlp_traces_endpoint.replace(
|
78
|
+
"/v1/traces", "/v1/logs"
|
79
|
+
)
|
80
|
+
else:
|
81
|
+
logs_endpoint = f"{config.otlp_endpoint}/v1/logs"
|
82
|
+
|
83
|
+
exporter = OTLPLogExporter(
|
84
|
+
endpoint=logs_endpoint,
|
85
|
+
headers=headers,
|
86
|
+
)
|
87
|
+
|
88
|
+
# Create logger provider
|
89
|
+
logger_provider = LoggerProvider(resource=resource)
|
90
|
+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
|
91
|
+
|
92
|
+
# Get logger and emit log
|
93
|
+
otel_logger = logger_provider.get_logger(__name__)
|
94
|
+
|
95
|
+
# Add trace context if available
|
96
|
+
log_attrs = attributes or {}
|
97
|
+
current_span = trace.get_current_span()
|
98
|
+
if current_span and current_span.is_recording():
|
99
|
+
span_context = current_span.get_span_context()
|
100
|
+
log_attrs["trace_id"] = f"{span_context.trace_id:032x}"
|
101
|
+
log_attrs["span_id"] = f"{span_context.span_id:016x}"
|
102
|
+
|
103
|
+
# Map level to severity number
|
104
|
+
severity_map = {
|
105
|
+
"TRACE": 1,
|
106
|
+
"DEBUG": 5,
|
107
|
+
"INFO": 9,
|
108
|
+
"WARN": 13,
|
109
|
+
"WARNING": 13,
|
110
|
+
"ERROR": 17,
|
111
|
+
"FATAL": 21,
|
112
|
+
"CRITICAL": 21,
|
113
|
+
}
|
114
|
+
severity = severity_map.get(level.upper(), 9)
|
115
|
+
|
116
|
+
# Emit log record
|
117
|
+
otel_logger.emit(
|
118
|
+
severity_number=severity,
|
119
|
+
severity_text=level.upper(),
|
120
|
+
body=message,
|
121
|
+
attributes=log_attrs,
|
122
|
+
)
|
123
|
+
|
124
|
+
# Force flush to ensure delivery
|
125
|
+
logger_provider.force_flush()
|
126
|
+
|
127
|
+
log.debug(f"Sent log via OTLP: {message[:50]}...")
|
128
|
+
return True
|
129
|
+
|
130
|
+
except Exception as e:
|
131
|
+
log.debug(f"Failed to send via OTLP: {e}")
|
132
|
+
return False
|
133
|
+
|
134
|
+
|
135
|
+
def send_log_bulk(
|
136
|
+
message: str,
|
137
|
+
level: str = "INFO",
|
138
|
+
service: str | None = None,
|
139
|
+
attributes: dict[str, Any] | None = None,
|
140
|
+
client: OpenObserveClient | None = None,
|
141
|
+
) -> bool:
|
142
|
+
"""Send a log via OpenObserve bulk API.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
message: Log message
|
146
|
+
level: Log level
|
147
|
+
service: Service name
|
148
|
+
attributes: Additional attributes
|
149
|
+
client: OpenObserve client (creates new if not provided)
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
True if sent successfully
|
153
|
+
"""
|
154
|
+
try:
|
155
|
+
if client is None:
|
156
|
+
client = OpenObserveClient.from_config()
|
157
|
+
|
158
|
+
from provide.foundation.logger.config import TelemetryConfig
|
159
|
+
|
160
|
+
config = TelemetryConfig.from_env()
|
161
|
+
|
162
|
+
# Build log entry
|
163
|
+
log_entry = {
|
164
|
+
"_timestamp": int(datetime.now().timestamp() * 1_000_000),
|
165
|
+
"level": level.upper(),
|
166
|
+
"message": message,
|
167
|
+
"service": service or config.service_name or "foundation",
|
168
|
+
}
|
169
|
+
|
170
|
+
# Add attributes
|
171
|
+
if attributes:
|
172
|
+
log_entry.update(attributes)
|
173
|
+
|
174
|
+
# Add trace context if available
|
175
|
+
try:
|
176
|
+
from opentelemetry import trace
|
177
|
+
|
178
|
+
current_span = trace.get_current_span()
|
179
|
+
if current_span and current_span.is_recording():
|
180
|
+
span_context = current_span.get_span_context()
|
181
|
+
log_entry["trace_id"] = f"{span_context.trace_id:032x}"
|
182
|
+
log_entry["span_id"] = f"{span_context.span_id:016x}"
|
183
|
+
except ImportError:
|
184
|
+
pass
|
185
|
+
|
186
|
+
# Try Foundation's tracer context
|
187
|
+
try:
|
188
|
+
from provide.foundation.tracer.context import (
|
189
|
+
get_current_span,
|
190
|
+
get_current_trace_id,
|
191
|
+
)
|
192
|
+
|
193
|
+
span = get_current_span()
|
194
|
+
if span:
|
195
|
+
log_entry["trace_id"] = span.trace_id
|
196
|
+
log_entry["span_id"] = span.span_id
|
197
|
+
elif trace_id := get_current_trace_id():
|
198
|
+
log_entry["trace_id"] = trace_id
|
199
|
+
except ImportError:
|
200
|
+
pass
|
201
|
+
|
202
|
+
# Format as bulk request
|
203
|
+
stream = config.openobserve_stream
|
204
|
+
bulk_data = (
|
205
|
+
json.dumps({"index": {"_index": stream}})
|
206
|
+
+ "\n"
|
207
|
+
+ json.dumps(log_entry)
|
208
|
+
+ "\n"
|
209
|
+
)
|
210
|
+
|
211
|
+
# Send via bulk API
|
212
|
+
import requests
|
213
|
+
|
214
|
+
# Build URL - check if client.url already includes /api/{org}
|
215
|
+
if f"/api/{client.organization}" in client.url:
|
216
|
+
url = f"{client.url}/_bulk"
|
217
|
+
else:
|
218
|
+
url = f"{client.url}/api/{client.organization}/_bulk"
|
219
|
+
|
220
|
+
response = requests.post(
|
221
|
+
url,
|
222
|
+
headers=client.session.headers,
|
223
|
+
data=bulk_data,
|
224
|
+
timeout=client.timeout,
|
225
|
+
)
|
226
|
+
|
227
|
+
if response.status_code == 200:
|
228
|
+
log.debug(f"Sent log via bulk API: {message[:50]}...")
|
229
|
+
return True
|
230
|
+
else:
|
231
|
+
log.debug(f"Failed to send via bulk API: {response.status_code}")
|
232
|
+
return False
|
233
|
+
|
234
|
+
except Exception as e:
|
235
|
+
log.debug(f"Failed to send via bulk API: {e}")
|
236
|
+
return False
|
237
|
+
|
238
|
+
|
239
|
+
def send_log(
|
240
|
+
message: str,
|
241
|
+
level: str = "INFO",
|
242
|
+
service: str | None = None,
|
243
|
+
attributes: dict[str, Any] | None = None,
|
244
|
+
prefer_otlp: bool = True,
|
245
|
+
client: OpenObserveClient | None = None,
|
246
|
+
) -> bool:
|
247
|
+
"""Send a log using OTLP if available, otherwise bulk API.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
message: Log message
|
251
|
+
level: Log level
|
252
|
+
service: Service name
|
253
|
+
attributes: Additional attributes
|
254
|
+
prefer_otlp: Try OTLP first if True
|
255
|
+
client: OpenObserve client for bulk API
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
True if sent successfully
|
259
|
+
"""
|
260
|
+
# Try OTLP first if preferred and available
|
261
|
+
if prefer_otlp and _HAS_OTEL_LOGS:
|
262
|
+
if send_log_otlp(message, level, service, attributes):
|
263
|
+
return True
|
264
|
+
|
265
|
+
# Fall back to bulk API
|
266
|
+
return send_log_bulk(message, level, service, attributes, client)
|
267
|
+
|
268
|
+
|
269
|
+
def create_otlp_logger_provider() -> Any | None:
|
270
|
+
"""Create an OTLP logger provider for continuous logging.
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
LoggerProvider if OTLP is available and configured, None otherwise
|
274
|
+
"""
|
275
|
+
if not _HAS_OTEL_LOGS:
|
276
|
+
return None
|
277
|
+
|
278
|
+
try:
|
279
|
+
from provide.foundation.logger.config import TelemetryConfig
|
280
|
+
|
281
|
+
config = TelemetryConfig.from_env()
|
282
|
+
|
283
|
+
if not config.otlp_endpoint:
|
284
|
+
return None
|
285
|
+
|
286
|
+
# Create resource
|
287
|
+
resource_attrs = {
|
288
|
+
ResourceAttributes.SERVICE_NAME: config.service_name or "foundation",
|
289
|
+
}
|
290
|
+
if config.service_version:
|
291
|
+
resource_attrs[ResourceAttributes.SERVICE_VERSION] = config.service_version
|
292
|
+
|
293
|
+
resource = Resource.create(resource_attrs)
|
294
|
+
|
295
|
+
# Configure exporter
|
296
|
+
headers = config.get_otlp_headers_dict()
|
297
|
+
if config.openobserve_org:
|
298
|
+
headers["organization"] = config.openobserve_org
|
299
|
+
headers["stream-name"] = config.openobserve_stream
|
300
|
+
|
301
|
+
logs_endpoint = f"{config.otlp_endpoint}/v1/logs"
|
302
|
+
if config.otlp_traces_endpoint:
|
303
|
+
logs_endpoint = config.otlp_traces_endpoint.replace(
|
304
|
+
"/v1/traces", "/v1/logs"
|
305
|
+
)
|
306
|
+
|
307
|
+
exporter = OTLPLogExporter(
|
308
|
+
endpoint=logs_endpoint,
|
309
|
+
headers=headers,
|
310
|
+
)
|
311
|
+
|
312
|
+
# Create provider
|
313
|
+
logger_provider = LoggerProvider(resource=resource)
|
314
|
+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
|
315
|
+
|
316
|
+
return logger_provider
|
317
|
+
|
318
|
+
except Exception as e:
|
319
|
+
log.debug(f"Failed to create OTLP logger provider: {e}")
|
320
|
+
return None
|
@@ -0,0 +1,222 @@
|
|
1
|
+
"""
|
2
|
+
Search operations for OpenObserve.
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
from provide.foundation.logger import get_logger
|
7
|
+
from provide.foundation.observability.openobserve.client import OpenObserveClient
|
8
|
+
from provide.foundation.observability.openobserve.models import SearchResponse
|
9
|
+
|
10
|
+
log = get_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def search_logs(
|
14
|
+
sql: str,
|
15
|
+
start_time: str | int | None = None,
|
16
|
+
end_time: str | int | None = None,
|
17
|
+
size: int = 100,
|
18
|
+
client: OpenObserveClient | None = None,
|
19
|
+
) -> SearchResponse:
|
20
|
+
"""Search logs in OpenObserve.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
sql: SQL query to execute
|
24
|
+
start_time: Start time (relative like "-1h" or microseconds)
|
25
|
+
end_time: End time (relative like "now" or microseconds)
|
26
|
+
size: Number of results to return
|
27
|
+
client: OpenObserve client (creates new if not provided)
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
SearchResponse with results
|
31
|
+
"""
|
32
|
+
if client is None:
|
33
|
+
client = OpenObserveClient.from_config()
|
34
|
+
|
35
|
+
return client.search(
|
36
|
+
sql=sql,
|
37
|
+
start_time=start_time,
|
38
|
+
end_time=end_time,
|
39
|
+
size=size,
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def search_by_trace_id(
|
44
|
+
trace_id: str,
|
45
|
+
stream: str = "default",
|
46
|
+
client: OpenObserveClient | None = None,
|
47
|
+
) -> SearchResponse:
|
48
|
+
"""Search for logs by trace ID.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
trace_id: Trace ID to search for
|
52
|
+
stream: Stream name to search in
|
53
|
+
client: OpenObserve client (creates new if not provided)
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
SearchResponse with matching logs
|
57
|
+
"""
|
58
|
+
sql = (
|
59
|
+
f"SELECT * FROM {stream} WHERE trace_id = '{trace_id}' ORDER BY _timestamp ASC"
|
60
|
+
)
|
61
|
+
return search_logs(sql=sql, start_time="-24h", client=client)
|
62
|
+
|
63
|
+
|
64
|
+
def search_by_level(
|
65
|
+
level: str,
|
66
|
+
stream: str = "default",
|
67
|
+
start_time: str | int | None = None,
|
68
|
+
end_time: str | int | None = None,
|
69
|
+
size: int = 100,
|
70
|
+
client: OpenObserveClient | None = None,
|
71
|
+
) -> SearchResponse:
|
72
|
+
"""Search for logs by level.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
level: Log level to filter (ERROR, WARN, INFO, DEBUG, etc.)
|
76
|
+
stream: Stream name to search in
|
77
|
+
start_time: Start time
|
78
|
+
end_time: End time
|
79
|
+
size: Number of results
|
80
|
+
client: OpenObserve client
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
SearchResponse with matching logs
|
84
|
+
"""
|
85
|
+
sql = f"SELECT * FROM {stream} WHERE level = '{level}' ORDER BY _timestamp DESC"
|
86
|
+
return search_logs(
|
87
|
+
sql=sql,
|
88
|
+
start_time=start_time,
|
89
|
+
end_time=end_time,
|
90
|
+
size=size,
|
91
|
+
client=client,
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
def search_errors(
|
96
|
+
stream: str = "default",
|
97
|
+
start_time: str | int | None = None,
|
98
|
+
size: int = 100,
|
99
|
+
client: OpenObserveClient | None = None,
|
100
|
+
) -> SearchResponse:
|
101
|
+
"""Search for error logs.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
stream: Stream name to search in
|
105
|
+
start_time: Start time
|
106
|
+
size: Number of results
|
107
|
+
client: OpenObserve client
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
SearchResponse with error logs
|
111
|
+
"""
|
112
|
+
return search_by_level(
|
113
|
+
level="ERROR",
|
114
|
+
stream=stream,
|
115
|
+
start_time=start_time,
|
116
|
+
size=size,
|
117
|
+
client=client,
|
118
|
+
)
|
119
|
+
|
120
|
+
|
121
|
+
def search_by_service(
|
122
|
+
service: str,
|
123
|
+
stream: str = "default",
|
124
|
+
start_time: str | int | None = None,
|
125
|
+
end_time: str | int | None = None,
|
126
|
+
size: int = 100,
|
127
|
+
client: OpenObserveClient | None = None,
|
128
|
+
) -> SearchResponse:
|
129
|
+
"""Search for logs by service name.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
service: Service name to filter
|
133
|
+
stream: Stream name to search in
|
134
|
+
start_time: Start time
|
135
|
+
end_time: End time
|
136
|
+
size: Number of results
|
137
|
+
client: OpenObserve client
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
SearchResponse with matching logs
|
141
|
+
"""
|
142
|
+
sql = f"SELECT * FROM {stream} WHERE service = '{service}' ORDER BY _timestamp DESC"
|
143
|
+
return search_logs(
|
144
|
+
sql=sql,
|
145
|
+
start_time=start_time,
|
146
|
+
end_time=end_time,
|
147
|
+
size=size,
|
148
|
+
client=client,
|
149
|
+
)
|
150
|
+
|
151
|
+
|
152
|
+
def aggregate_by_level(
|
153
|
+
stream: str = "default",
|
154
|
+
start_time: str | int | None = None,
|
155
|
+
end_time: str | int | None = None,
|
156
|
+
client: OpenObserveClient | None = None,
|
157
|
+
) -> dict[str, int]:
|
158
|
+
"""Get count of logs by level.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
stream: Stream name to search in
|
162
|
+
start_time: Start time
|
163
|
+
end_time: End time
|
164
|
+
client: OpenObserve client
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Dictionary mapping level to count
|
168
|
+
"""
|
169
|
+
sql = f"SELECT level, COUNT(*) as count FROM {stream} GROUP BY level"
|
170
|
+
response = search_logs(
|
171
|
+
sql=sql,
|
172
|
+
start_time=start_time,
|
173
|
+
end_time=end_time,
|
174
|
+
size=1000,
|
175
|
+
client=client,
|
176
|
+
)
|
177
|
+
|
178
|
+
result = {}
|
179
|
+
for hit in response.hits:
|
180
|
+
level = hit.get("level", "UNKNOWN")
|
181
|
+
count = hit.get("count", 0)
|
182
|
+
result[level] = count
|
183
|
+
|
184
|
+
return result
|
185
|
+
|
186
|
+
|
187
|
+
def get_current_trace_logs(
|
188
|
+
stream: str = "default",
|
189
|
+
client: OpenObserveClient | None = None,
|
190
|
+
) -> SearchResponse | None:
|
191
|
+
"""Get logs for the current active trace.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
stream: Stream name to search in
|
195
|
+
client: OpenObserve client
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
SearchResponse with logs for current trace, or None if no active trace
|
199
|
+
"""
|
200
|
+
# Try to get current trace ID from OpenTelemetry
|
201
|
+
try:
|
202
|
+
from opentelemetry import trace
|
203
|
+
|
204
|
+
current_span = trace.get_current_span()
|
205
|
+
if current_span and current_span.is_recording():
|
206
|
+
span_context = current_span.get_span_context()
|
207
|
+
trace_id = f"{span_context.trace_id:032x}"
|
208
|
+
return search_by_trace_id(trace_id, stream=stream, client=client)
|
209
|
+
except ImportError:
|
210
|
+
pass
|
211
|
+
|
212
|
+
# Try to get from Foundation tracer
|
213
|
+
try:
|
214
|
+
from provide.foundation.tracer.context import get_current_trace_id
|
215
|
+
|
216
|
+
trace_id = get_current_trace_id()
|
217
|
+
if trace_id:
|
218
|
+
return search_by_trace_id(trace_id, stream=stream, client=client)
|
219
|
+
except ImportError:
|
220
|
+
pass
|
221
|
+
|
222
|
+
return None
|