gcp-observability 0.1.0__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.
- gcp_observability-0.1.0/.claude/settings.local.json +17 -0
- gcp_observability-0.1.0/.gitignore +8 -0
- gcp_observability-0.1.0/.python-version +1 -0
- gcp_observability-0.1.0/LICENSE +21 -0
- gcp_observability-0.1.0/PKG-INFO +163 -0
- gcp_observability-0.1.0/README.md +141 -0
- gcp_observability-0.1.0/gcp_observability/__init__.py +3 -0
- gcp_observability-0.1.0/gcp_observability/logging/__init__.py +17 -0
- gcp_observability-0.1.0/gcp_observability/logging/constants.py +40 -0
- gcp_observability-0.1.0/gcp_observability/logging/expressions.py +175 -0
- gcp_observability-0.1.0/gcp_observability/logging/query.py +286 -0
- gcp_observability-0.1.0/pyproject.toml +44 -0
- gcp_observability-0.1.0/tests/__init__.py +0 -0
- gcp_observability-0.1.0/tests/test_expressions.py +206 -0
- gcp_observability-0.1.0/tests/test_query.py +303 -0
- gcp_observability-0.1.0/uv.lock +532 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(gcloud config *)",
|
|
5
|
+
"Bash(gcloud projects *)",
|
|
6
|
+
"Bash(.venv/bin/python *)",
|
|
7
|
+
"Bash(git commit *)",
|
|
8
|
+
"Bash(git -C /Users/javad/Desktop/gcp_observability add gcp_observability/logs/ main.py)",
|
|
9
|
+
"Bash(git -C /Users/javad/Desktop/gcp_observability commit -m ' *)",
|
|
10
|
+
"Bash(git -C /Users/javad/Desktop/gcp_observability push)",
|
|
11
|
+
"Bash(git -C /Users/javad/Desktop/gcp_observability add gcp_observability/discovery/cloud_run.py)",
|
|
12
|
+
"Bash(.venv/bin/pytest *)",
|
|
13
|
+
"Bash(git config *)",
|
|
14
|
+
"Bash(uv build *)"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 javadebadi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gcp-observability
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Programmatic query builder for Google Cloud Logging filter language
|
|
5
|
+
Project-URL: Repository, https://github.com/javadebadi/gcp-observability
|
|
6
|
+
Author-email: javadebadi <javad.ebadi.1990@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cloud-logging,gcp,google-cloud,logging,observability,query-builder
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Logging
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: google-cloud-logging>=3.16.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# GCP Observability
|
|
24
|
+
|
|
25
|
+
Python toolkit for Google Cloud observability — starting with a programmatic Cloud Logging query builder.
|
|
26
|
+
|
|
27
|
+
## Cloud Logging Query Builder
|
|
28
|
+
|
|
29
|
+
Build Cloud Logging filter strings in Python instead of writing them by hand.
|
|
30
|
+
The output is identical to what you'd type in the **"Build query"** panel of the Cloud Logging console.
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Example — find errors containing "ValueError: Bad"
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
from gcp_observability.logging import QueryBuilder, Severity, F
|
|
43
|
+
|
|
44
|
+
query = (
|
|
45
|
+
QueryBuilder()
|
|
46
|
+
.severity_gte(Severity.ERROR)
|
|
47
|
+
.time_range(
|
|
48
|
+
start=datetime(2026, 7, 9, 10, 0, 0), # 2026-07-09T10:00:00Z
|
|
49
|
+
end=datetime(2026, 7, 9, 10, 30, 0), # 2026-07-09T10:30:00Z
|
|
50
|
+
)
|
|
51
|
+
.where(
|
|
52
|
+
F("textPayload").has("ValueError: Bad")
|
|
53
|
+
| F("jsonPayload.message").has("ValueError: Bad")
|
|
54
|
+
)
|
|
55
|
+
.build()
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(query)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Output — paste this directly into the Cloud Logging console or pass to `gcloud`:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
severity>=ERROR
|
|
65
|
+
timestamp>="2026-07-09T10:00:00Z"
|
|
66
|
+
timestamp<"2026-07-09T10:30:00Z"
|
|
67
|
+
(textPayload:"ValueError: Bad") OR (jsonPayload.message:"ValueError: Bad")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Use with the Cloud Logging client
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import google.cloud.logging
|
|
74
|
+
|
|
75
|
+
client = google.cloud.logging.Client(project="my-gcp-project")
|
|
76
|
+
entries = client.list_entries(filter_=query)
|
|
77
|
+
|
|
78
|
+
for entry in entries:
|
|
79
|
+
print(entry.timestamp, entry.payload)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Use with gcloud CLI
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
gcloud logging read 'severity>=ERROR
|
|
86
|
+
timestamp>="2026-07-09T10:00:00Z"
|
|
87
|
+
timestamp<"2026-07-09T10:30:00Z"
|
|
88
|
+
(textPayload:"ValueError: Bad") OR (jsonPayload.message:"ValueError: Bad")'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## API reference
|
|
94
|
+
|
|
95
|
+
### `QueryBuilder` methods
|
|
96
|
+
|
|
97
|
+
| Method | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `.resource_type(type)` | Filter by resource type (e.g. `cloud_run_revision`) |
|
|
100
|
+
| `.resource_label(key, value)` | Filter by a resource label |
|
|
101
|
+
| `.project(project_id)` | Restrict to a GCP project |
|
|
102
|
+
| `.log_name(name)` | Exact `logName` match |
|
|
103
|
+
| `.severity(op, level)` | Severity with explicit operator (`=`, `>=`, etc.) |
|
|
104
|
+
| `.severity_eq(level)` | `severity=LEVEL` |
|
|
105
|
+
| `.severity_gte(level)` | `severity>=LEVEL` |
|
|
106
|
+
| `.severity_range(low, high)` | `severity>=LOW` and `severity<=HIGH` |
|
|
107
|
+
| `.time_range(start, end)` | Timestamp window (str or `datetime`, end is exclusive) |
|
|
108
|
+
| `.since(hours, minutes, days)` | Relative window from now |
|
|
109
|
+
| `.text_payload(value)` | Substring match on `textPayload` |
|
|
110
|
+
| `.json_payload(field, op, value)` | Filter on a `jsonPayload` sub-field |
|
|
111
|
+
| `.json_payload_has(field, value)` | Substring match on a `jsonPayload` sub-field |
|
|
112
|
+
| `.proto_payload(field, op, value)` | Filter on a `protoPayload` sub-field |
|
|
113
|
+
| `.http_method(method)` | HTTP request method |
|
|
114
|
+
| `.http_status(op, status)` | HTTP response status code |
|
|
115
|
+
| `.http_url(value)` | Substring match on request URL |
|
|
116
|
+
| `.http_latency_gte(seconds)` | Requests slower than N seconds |
|
|
117
|
+
| `.label(key, value)` | Log entry label (special chars in key auto-quoted) |
|
|
118
|
+
| `.trace(trace_id)` | Filter by trace ID |
|
|
119
|
+
| `.span_id(span_id)` | Filter by span ID |
|
|
120
|
+
| `.where(expr)` | Add a raw `F()`-built expression |
|
|
121
|
+
| `.raw(filter_str)` | Add a verbatim filter string |
|
|
122
|
+
| `.build()` | Return the complete filter string |
|
|
123
|
+
|
|
124
|
+
### `F()` — low-level expression builder
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from gcp_observability.logging import F
|
|
128
|
+
|
|
129
|
+
# Comparisons
|
|
130
|
+
F("severity") >= "ERROR"
|
|
131
|
+
F("resource.type") == "cloud_run_revision"
|
|
132
|
+
F("httpRequest.status") >= 500
|
|
133
|
+
|
|
134
|
+
# Substring / has-field
|
|
135
|
+
F("textPayload").has("panic")
|
|
136
|
+
F("jsonPayload.message").has("timeout")
|
|
137
|
+
|
|
138
|
+
# Chained dot access
|
|
139
|
+
F("resource").labels.zone == "us-central1-a"
|
|
140
|
+
|
|
141
|
+
# Labels with special characters
|
|
142
|
+
F("labels")["k8s-pod/app"] == "my-service"
|
|
143
|
+
|
|
144
|
+
# Boolean operators
|
|
145
|
+
expr_a & expr_b # AND
|
|
146
|
+
expr_a | expr_b # OR
|
|
147
|
+
~expr_a # NOT
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Constants
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from gcp_observability.logging import Severity, ResourceType
|
|
154
|
+
|
|
155
|
+
Severity.DEBUG, Severity.INFO, Severity.NOTICE
|
|
156
|
+
Severity.WARNING, Severity.ERROR, Severity.CRITICAL
|
|
157
|
+
|
|
158
|
+
ResourceType.CLOUD_RUN_REVISION
|
|
159
|
+
ResourceType.GKE_CONTAINER
|
|
160
|
+
ResourceType.GCE_INSTANCE
|
|
161
|
+
ResourceType.LOAD_BALANCER
|
|
162
|
+
# ... see constants.py for full list
|
|
163
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# GCP Observability
|
|
2
|
+
|
|
3
|
+
Python toolkit for Google Cloud observability — starting with a programmatic Cloud Logging query builder.
|
|
4
|
+
|
|
5
|
+
## Cloud Logging Query Builder
|
|
6
|
+
|
|
7
|
+
Build Cloud Logging filter strings in Python instead of writing them by hand.
|
|
8
|
+
The output is identical to what you'd type in the **"Build query"** panel of the Cloud Logging console.
|
|
9
|
+
|
|
10
|
+
### Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Example — find errors containing "ValueError: Bad"
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from gcp_observability.logging import QueryBuilder, Severity, F
|
|
21
|
+
|
|
22
|
+
query = (
|
|
23
|
+
QueryBuilder()
|
|
24
|
+
.severity_gte(Severity.ERROR)
|
|
25
|
+
.time_range(
|
|
26
|
+
start=datetime(2026, 7, 9, 10, 0, 0), # 2026-07-09T10:00:00Z
|
|
27
|
+
end=datetime(2026, 7, 9, 10, 30, 0), # 2026-07-09T10:30:00Z
|
|
28
|
+
)
|
|
29
|
+
.where(
|
|
30
|
+
F("textPayload").has("ValueError: Bad")
|
|
31
|
+
| F("jsonPayload.message").has("ValueError: Bad")
|
|
32
|
+
)
|
|
33
|
+
.build()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
print(query)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Output — paste this directly into the Cloud Logging console or pass to `gcloud`:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
severity>=ERROR
|
|
43
|
+
timestamp>="2026-07-09T10:00:00Z"
|
|
44
|
+
timestamp<"2026-07-09T10:30:00Z"
|
|
45
|
+
(textPayload:"ValueError: Bad") OR (jsonPayload.message:"ValueError: Bad")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Use with the Cloud Logging client
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import google.cloud.logging
|
|
52
|
+
|
|
53
|
+
client = google.cloud.logging.Client(project="my-gcp-project")
|
|
54
|
+
entries = client.list_entries(filter_=query)
|
|
55
|
+
|
|
56
|
+
for entry in entries:
|
|
57
|
+
print(entry.timestamp, entry.payload)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Use with gcloud CLI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gcloud logging read 'severity>=ERROR
|
|
64
|
+
timestamp>="2026-07-09T10:00:00Z"
|
|
65
|
+
timestamp<"2026-07-09T10:30:00Z"
|
|
66
|
+
(textPayload:"ValueError: Bad") OR (jsonPayload.message:"ValueError: Bad")'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## API reference
|
|
72
|
+
|
|
73
|
+
### `QueryBuilder` methods
|
|
74
|
+
|
|
75
|
+
| Method | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `.resource_type(type)` | Filter by resource type (e.g. `cloud_run_revision`) |
|
|
78
|
+
| `.resource_label(key, value)` | Filter by a resource label |
|
|
79
|
+
| `.project(project_id)` | Restrict to a GCP project |
|
|
80
|
+
| `.log_name(name)` | Exact `logName` match |
|
|
81
|
+
| `.severity(op, level)` | Severity with explicit operator (`=`, `>=`, etc.) |
|
|
82
|
+
| `.severity_eq(level)` | `severity=LEVEL` |
|
|
83
|
+
| `.severity_gte(level)` | `severity>=LEVEL` |
|
|
84
|
+
| `.severity_range(low, high)` | `severity>=LOW` and `severity<=HIGH` |
|
|
85
|
+
| `.time_range(start, end)` | Timestamp window (str or `datetime`, end is exclusive) |
|
|
86
|
+
| `.since(hours, minutes, days)` | Relative window from now |
|
|
87
|
+
| `.text_payload(value)` | Substring match on `textPayload` |
|
|
88
|
+
| `.json_payload(field, op, value)` | Filter on a `jsonPayload` sub-field |
|
|
89
|
+
| `.json_payload_has(field, value)` | Substring match on a `jsonPayload` sub-field |
|
|
90
|
+
| `.proto_payload(field, op, value)` | Filter on a `protoPayload` sub-field |
|
|
91
|
+
| `.http_method(method)` | HTTP request method |
|
|
92
|
+
| `.http_status(op, status)` | HTTP response status code |
|
|
93
|
+
| `.http_url(value)` | Substring match on request URL |
|
|
94
|
+
| `.http_latency_gte(seconds)` | Requests slower than N seconds |
|
|
95
|
+
| `.label(key, value)` | Log entry label (special chars in key auto-quoted) |
|
|
96
|
+
| `.trace(trace_id)` | Filter by trace ID |
|
|
97
|
+
| `.span_id(span_id)` | Filter by span ID |
|
|
98
|
+
| `.where(expr)` | Add a raw `F()`-built expression |
|
|
99
|
+
| `.raw(filter_str)` | Add a verbatim filter string |
|
|
100
|
+
| `.build()` | Return the complete filter string |
|
|
101
|
+
|
|
102
|
+
### `F()` — low-level expression builder
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from gcp_observability.logging import F
|
|
106
|
+
|
|
107
|
+
# Comparisons
|
|
108
|
+
F("severity") >= "ERROR"
|
|
109
|
+
F("resource.type") == "cloud_run_revision"
|
|
110
|
+
F("httpRequest.status") >= 500
|
|
111
|
+
|
|
112
|
+
# Substring / has-field
|
|
113
|
+
F("textPayload").has("panic")
|
|
114
|
+
F("jsonPayload.message").has("timeout")
|
|
115
|
+
|
|
116
|
+
# Chained dot access
|
|
117
|
+
F("resource").labels.zone == "us-central1-a"
|
|
118
|
+
|
|
119
|
+
# Labels with special characters
|
|
120
|
+
F("labels")["k8s-pod/app"] == "my-service"
|
|
121
|
+
|
|
122
|
+
# Boolean operators
|
|
123
|
+
expr_a & expr_b # AND
|
|
124
|
+
expr_a | expr_b # OR
|
|
125
|
+
~expr_a # NOT
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Constants
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from gcp_observability.logging import Severity, ResourceType
|
|
132
|
+
|
|
133
|
+
Severity.DEBUG, Severity.INFO, Severity.NOTICE
|
|
134
|
+
Severity.WARNING, Severity.ERROR, Severity.CRITICAL
|
|
135
|
+
|
|
136
|
+
ResourceType.CLOUD_RUN_REVISION
|
|
137
|
+
ResourceType.GKE_CONTAINER
|
|
138
|
+
ResourceType.GCE_INSTANCE
|
|
139
|
+
ResourceType.LOAD_BALANCER
|
|
140
|
+
# ... see constants.py for full list
|
|
141
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .constants import ResourceType, Severity
|
|
2
|
+
from .expressions import And, Comparison, Expr, F, Field, Not, Or, Raw
|
|
3
|
+
from .query import QueryBuilder
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"QueryBuilder",
|
|
7
|
+
"Expr",
|
|
8
|
+
"Comparison",
|
|
9
|
+
"And",
|
|
10
|
+
"Or",
|
|
11
|
+
"Not",
|
|
12
|
+
"Raw",
|
|
13
|
+
"Field",
|
|
14
|
+
"F",
|
|
15
|
+
"Severity",
|
|
16
|
+
"ResourceType",
|
|
17
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Cloud Logging constants: severity levels and resource types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Severity:
|
|
7
|
+
DEFAULT = "DEFAULT"
|
|
8
|
+
DEBUG = "DEBUG"
|
|
9
|
+
INFO = "INFO"
|
|
10
|
+
NOTICE = "NOTICE"
|
|
11
|
+
WARNING = "WARNING"
|
|
12
|
+
ERROR = "ERROR"
|
|
13
|
+
CRITICAL = "CRITICAL"
|
|
14
|
+
ALERT = "ALERT"
|
|
15
|
+
EMERGENCY = "EMERGENCY"
|
|
16
|
+
|
|
17
|
+
# Ordered list (lowest → highest) for range helpers
|
|
18
|
+
_LEVELS = [DEFAULT, DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ResourceType:
|
|
22
|
+
GLOBAL = "global"
|
|
23
|
+
GCE_INSTANCE = "gce_instance"
|
|
24
|
+
GKE_CONTAINER = "k8s_container"
|
|
25
|
+
GKE_POD = "k8s_pod"
|
|
26
|
+
GKE_NODE = "k8s_node"
|
|
27
|
+
GKE_CLUSTER = "k8s_cluster"
|
|
28
|
+
CLOUD_RUN_REVISION = "cloud_run_revision"
|
|
29
|
+
CLOUD_RUN_JOB = "cloud_run_job"
|
|
30
|
+
CLOUD_FUNCTION = "cloud_function"
|
|
31
|
+
APP_ENGINE = "gae_app"
|
|
32
|
+
CLOUD_SQL = "cloudsql_database"
|
|
33
|
+
LOAD_BALANCER = "http_load_balancer"
|
|
34
|
+
PUBSUB_TOPIC = "pubsub_topic"
|
|
35
|
+
PUBSUB_SUBSCRIPTION = "pubsub_subscription"
|
|
36
|
+
BIG_QUERY = "bigquery_resource"
|
|
37
|
+
STORAGE = "gcs_bucket"
|
|
38
|
+
DATAFLOW = "dataflow_step"
|
|
39
|
+
CLOUD_TASKS_QUEUE = "cloud_tasks_queue"
|
|
40
|
+
REDIS_INSTANCE = "redis_instance"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level expression tree for Cloud Logging filter language.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from gcp_observability.logging.expressions import F, And, Or, Not, Raw
|
|
6
|
+
|
|
7
|
+
expr = (
|
|
8
|
+
(F("resource.type") == "cloud_run_revision")
|
|
9
|
+
& (F("severity") >= "ERROR")
|
|
10
|
+
& ~(F("textPayload").has("healthcheck"))
|
|
11
|
+
)
|
|
12
|
+
print(expr.build())
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
_SAFE_VALUE = re.compile(r"^[a-zA-Z0-9_]+$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _format_value(value: object) -> str:
|
|
24
|
+
if isinstance(value, bool):
|
|
25
|
+
return "true" if value else "false"
|
|
26
|
+
if isinstance(value, (int, float)):
|
|
27
|
+
return str(value)
|
|
28
|
+
s = str(value)
|
|
29
|
+
if _SAFE_VALUE.match(s):
|
|
30
|
+
return s
|
|
31
|
+
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
|
|
32
|
+
return f'"{escaped}"'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Expr(ABC):
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def build(self) -> str: ...
|
|
38
|
+
|
|
39
|
+
def __and__(self, other: Expr) -> And:
|
|
40
|
+
if isinstance(self, And):
|
|
41
|
+
return And(*self.exprs, other)
|
|
42
|
+
return And(self, other)
|
|
43
|
+
|
|
44
|
+
def __or__(self, other: Expr) -> Or:
|
|
45
|
+
if isinstance(self, Or):
|
|
46
|
+
return Or(*self.exprs, other)
|
|
47
|
+
return Or(self, other)
|
|
48
|
+
|
|
49
|
+
def __invert__(self) -> Not:
|
|
50
|
+
return Not(self)
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return self.build()
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return f"{type(self).__name__}({self.build()!r})"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Comparison(Expr):
|
|
60
|
+
def __init__(self, field: str, op: str, value: object) -> None:
|
|
61
|
+
self.field = field
|
|
62
|
+
self.op = op
|
|
63
|
+
self.value = value
|
|
64
|
+
|
|
65
|
+
def build(self) -> str:
|
|
66
|
+
return f"{self.field}{self.op}{_format_value(self.value)}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class And(Expr):
|
|
70
|
+
def __init__(self, *exprs: Expr) -> None:
|
|
71
|
+
flat: list[Expr] = []
|
|
72
|
+
for e in exprs:
|
|
73
|
+
if isinstance(e, And):
|
|
74
|
+
flat.extend(e.exprs)
|
|
75
|
+
else:
|
|
76
|
+
flat.append(e)
|
|
77
|
+
self.exprs = tuple(flat)
|
|
78
|
+
|
|
79
|
+
def build(self) -> str:
|
|
80
|
+
parts: list[str] = []
|
|
81
|
+
for e in self.exprs:
|
|
82
|
+
s = e.build()
|
|
83
|
+
parts.append(f"({s})" if isinstance(e, Or) else s)
|
|
84
|
+
return "\nAND ".join(parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Or(Expr):
|
|
88
|
+
def __init__(self, *exprs: Expr) -> None:
|
|
89
|
+
flat: list[Expr] = []
|
|
90
|
+
for e in exprs:
|
|
91
|
+
if isinstance(e, Or):
|
|
92
|
+
flat.extend(e.exprs)
|
|
93
|
+
else:
|
|
94
|
+
flat.append(e)
|
|
95
|
+
self.exprs = tuple(flat)
|
|
96
|
+
|
|
97
|
+
def build(self) -> str:
|
|
98
|
+
return " OR ".join(f"({e.build()})" for e in self.exprs)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Not(Expr):
|
|
102
|
+
def __init__(self, expr: Expr) -> None:
|
|
103
|
+
self.expr = expr
|
|
104
|
+
|
|
105
|
+
def build(self) -> str:
|
|
106
|
+
inner = self.expr.build()
|
|
107
|
+
if isinstance(self.expr, (And, Or)):
|
|
108
|
+
return f"NOT ({inner})"
|
|
109
|
+
return f"NOT {inner}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Raw(Expr):
|
|
113
|
+
"""Pass a raw filter string through unchanged."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, filter_str: str) -> None:
|
|
116
|
+
self.filter_str = filter_str
|
|
117
|
+
|
|
118
|
+
def build(self) -> str:
|
|
119
|
+
return self.filter_str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Field:
|
|
123
|
+
"""
|
|
124
|
+
A field reference that produces Expr objects via comparison operators.
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
F("severity") >= "ERROR" -> Comparison("severity", ">=", "ERROR")
|
|
128
|
+
F("resource.type") == "gce_instance"
|
|
129
|
+
F("textPayload").has("error") -> Comparison("textPayload", ":", "error")
|
|
130
|
+
F("labels")["my-label"] == "v1" -> labels with special-char key
|
|
131
|
+
F("resource").labels.zone == "us-central1-a" # chained via dot
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
__slots__ = ("_name",)
|
|
135
|
+
|
|
136
|
+
def __init__(self, name: str) -> None:
|
|
137
|
+
object.__setattr__(self, "_name", name)
|
|
138
|
+
|
|
139
|
+
# Attribute access builds dot-notation field paths: F("resource").type
|
|
140
|
+
def __getattr__(self, attr: str) -> Field:
|
|
141
|
+
return Field(f"{self._name}.{attr}")
|
|
142
|
+
|
|
143
|
+
# Bracket access quotes the key for special-char label/field names
|
|
144
|
+
def __getitem__(self, key: str) -> Field:
|
|
145
|
+
return Field(f'{self._name}."{key}"')
|
|
146
|
+
|
|
147
|
+
def __eq__(self, value: object) -> Comparison: # type: ignore[override]
|
|
148
|
+
return Comparison(self._name, "=", value)
|
|
149
|
+
|
|
150
|
+
def __ne__(self, value: object) -> Comparison: # type: ignore[override]
|
|
151
|
+
return Comparison(self._name, "!=", value)
|
|
152
|
+
|
|
153
|
+
def __gt__(self, value: object) -> Comparison:
|
|
154
|
+
return Comparison(self._name, ">", value)
|
|
155
|
+
|
|
156
|
+
def __lt__(self, value: object) -> Comparison:
|
|
157
|
+
return Comparison(self._name, "<", value)
|
|
158
|
+
|
|
159
|
+
def __ge__(self, value: object) -> Comparison:
|
|
160
|
+
return Comparison(self._name, ">=", value)
|
|
161
|
+
|
|
162
|
+
def __le__(self, value: object) -> Comparison:
|
|
163
|
+
return Comparison(self._name, "<=", value)
|
|
164
|
+
|
|
165
|
+
def has(self, value: str) -> Comparison:
|
|
166
|
+
"""Substring / has-field match using the `:` operator."""
|
|
167
|
+
return Comparison(self._name, ":", value)
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"Field({self._name!r})"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def F(name: str) -> Field:
|
|
174
|
+
"""Shorthand for Field(name)."""
|
|
175
|
+
return Field(name)
|