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.
@@ -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,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .DS_Store
6
+ inventory.json
7
+ *.egg-info/
8
+ .ruff_cache/
@@ -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,3 @@
1
+ from .logging import QueryBuilder, Severity, ResourceType, F
2
+
3
+ __all__ = ["QueryBuilder", "Severity", "ResourceType", "F"]
@@ -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)