Skip to content

Commit 7e383fc

Browse files
Merge pull request #39 from DataKitchen/release/2.11.3
Release/2.11.3
2 parents 503643e + e241af9 commit 7e383fc

74 files changed

Lines changed: 989 additions & 135 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# DataOps Observability
2-
![apache 2.0 license Badge](https://img.shields.io/badge/License%20-%20Apache%202.0%20-%20blue) ![PRs Badge](https://img.shields.io/badge/PRs%20-%20Welcome%20-%20green) [![Docker Pulls](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fhub.docker.com%2Fv2%2Frepositories%2Fdatakitchen%2Fdataops-testgen%2F&query=pull_count&style=flat&label=docker%20pulls&color=06A04A)](https://hub.docker.com/r/datakitchen/dataops-observability) [![Documentation](https://img.shields.io/badge/docs-On%20datakitchen.io-06A04A?style=flat)](https://docs.datakitchen.io/articles/#!dataops-observability-help/dataops-observability-help)
2+
![apache 2.0 license Badge](https://img.shields.io/badge/License%20-%20Apache%202.0%20-%20blue) ![PRs Badge](https://img.shields.io/badge/PRs%20-%20Welcome%20-%20green) [![Docker Pulls](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fhub.docker.com%2Fv2%2Frepositories%2Fdatakitchen%2Fdataops-testgen%2F&query=pull_count&style=flat&label=docker%20pulls&color=06A04A)](https://hub.docker.com/r/datakitchen/dataops-observability) [![Documentation](https://img.shields.io/badge/docs-On%20datakitchen.io-06A04A?style=flat)](https://docs.datakitchen.io/observability/what-is-observability/)
33
[![Latest Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fhub.docker.com%2Fv2%2Frepositories%2Fdatakitchen%2Fdataops-observability-be%2Ftags%2F&query=results%5B0%5D.name&label=latest%20version&color=06A04A)](https://hub.docker.com/r/datakitchen/dataops-observability-be)
44
[![Static Badge](https://img.shields.io/badge/Slack-Join%20Discussion-blue?style=flat&logo=slack)](https://data-observability-slack.datakitchen.io/join)
55

@@ -147,7 +147,7 @@ To resolve this error, do two things:
147147
## Community
148148

149149
### Getting Started Guide
150-
We recommend you start by going through the [Data Observability Overview Demo](https://docs.datakitchen.io/articles/open-source-data-observability/data-observability-overview).
150+
We recommend you start by going through the [Data Observability Overview Demo](https://docs.datakitchen.io/tutorials/quickstart-demo/).
151151

152152
### Support
153153
For support requests, [join the Data Observability Slack](https://data-observability-slack.datakitchen.io/join) and ask post on #support channel.

common/entities/journey.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class Journey(BaseEntity, AuditEntityMixin, AuditUpdateTimeEntityMixin):
1717

1818
name = CharField(unique=False, null=True)
1919
description = CharField(null=True)
20+
component_include_patterns = CharField(null=True)
21+
component_exclude_patterns = CharField(null=True)
2022
project = ForeignKeyField(Project, backref="journeys", on_delete="CASCADE", null=False, index=True)
2123

2224
@staticmethod

common/entity_services/journey_service.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
__all__ = ["JourneyService"]
22

33
import logging
4+
from fnmatch import fnmatch
45
from typing import Optional
56
from uuid import UUID
67

7-
from common.entities import Action, Company, Component, Journey, JourneyDagEdge, Organization, Project, Rule
8+
from common.entities import DB, Action, Company, Component, Journey, JourneyDagEdge, Organization, Project, Rule
89
from common.entity_services.helpers import ComponentFilters, ListRules, Page
910
from common.exceptions.service import MultipleActionsFound
1011

@@ -64,6 +65,72 @@ def get_components_with_rules(journey_id: str, rules: ListRules, filters: Compon
6465

6566
return Page[Component].get_paginated_results(query.distinct(), Component.key, rules)
6667

68+
@staticmethod
69+
def parse_patterns(patterns_str: str | None) -> list[str]:
70+
if not patterns_str:
71+
return []
72+
return [p.strip() for p in patterns_str.replace("\n", ",").split(",") if p.strip()]
73+
74+
@staticmethod
75+
def get_components_matching_patterns(
76+
project_id: str, include_patterns: list[str], exclude_patterns: list[str]
77+
) -> list[Component]:
78+
if not include_patterns:
79+
return []
80+
components: list[Component] = list(Component.select().where(Component.project == project_id))
81+
return [
82+
c
83+
for c in components
84+
if any(fnmatch(c.key, p) for p in include_patterns) and not any(fnmatch(c.key, p) for p in exclude_patterns)
85+
]
86+
87+
@staticmethod
88+
def apply_component_patterns(journey: Journey) -> None:
89+
include_patterns = JourneyService.parse_patterns(journey.component_include_patterns)
90+
exclude_patterns = JourneyService.parse_patterns(journey.component_exclude_patterns)
91+
92+
with DB.atomic():
93+
existing_edges = list(
94+
JourneyDagEdge.select().where(JourneyDagEdge.journey == journey, JourneyDagEdge.left.is_null(False))
95+
)
96+
JourneyDagEdge.delete().where(JourneyDagEdge.journey == journey).execute()
97+
98+
if not include_patterns:
99+
return
100+
101+
matching = JourneyService.get_components_matching_patterns(
102+
journey.project_id, include_patterns, exclude_patterns
103+
)
104+
matching_ids = {c.id for c in matching}
105+
106+
restored_edges = [e for e in existing_edges if e.left_id in matching_ids and e.right_id in matching_ids]
107+
has_predecessor = {e.right_id for e in restored_edges}
108+
109+
for component in matching:
110+
if component.id not in has_predecessor:
111+
JourneyDagEdge(journey=journey, left=None, right=component).save(force_insert=True)
112+
113+
for edge in restored_edges:
114+
JourneyDagEdge(journey=journey, left=edge.left, right=edge.right).save(force_insert=True)
115+
116+
@staticmethod
117+
def add_component_to_matching_journeys(component: Component) -> None:
118+
journeys = list(
119+
Journey.select().where(
120+
Journey.project == component.project_id,
121+
Journey.component_include_patterns.is_null(False),
122+
)
123+
)
124+
for journey in journeys:
125+
include_patterns = JourneyService.parse_patterns(journey.component_include_patterns)
126+
exclude_patterns = JourneyService.parse_patterns(journey.component_exclude_patterns)
127+
if not include_patterns:
128+
continue
129+
if any(fnmatch(component.key, p) for p in include_patterns) and not any(
130+
fnmatch(component.key, p) for p in exclude_patterns
131+
):
132+
JourneyDagEdge(journey=journey, left=None, right=component).save(force_insert=True)
133+
67134
@staticmethod
68135
def get_upstream_nodes(journey: Journey, component_id: UUID) -> set:
69136
journey_dag = journey.journey_dag

common/tests/integration/entity_services/test_journey_service.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,218 @@
88
from common.exceptions.service import MultipleActionsFound
99

1010

11+
# --- parse_patterns ---
12+
13+
14+
@pytest.mark.unit
15+
def test_parse_patterns_none():
16+
assert JourneyService.parse_patterns(None) == []
17+
18+
19+
@pytest.mark.unit
20+
def test_parse_patterns_empty_string():
21+
assert JourneyService.parse_patterns("") == []
22+
23+
24+
@pytest.mark.unit
25+
def test_parse_patterns_comma_separated():
26+
assert JourneyService.parse_patterns("a,b,c") == ["a", "b", "c"]
27+
28+
29+
@pytest.mark.unit
30+
def test_parse_patterns_newline_separated():
31+
assert JourneyService.parse_patterns("a\nb\nc") == ["a", "b", "c"]
32+
33+
34+
@pytest.mark.unit
35+
def test_parse_patterns_trims_whitespace():
36+
assert JourneyService.parse_patterns(" a , b ") == ["a", "b"]
37+
38+
39+
@pytest.mark.unit
40+
def test_parse_patterns_skips_empty_entries():
41+
assert JourneyService.parse_patterns("a,,b") == ["a", "b"]
42+
43+
44+
# --- get_components_matching_patterns ---
45+
46+
47+
@pytest.mark.integration
48+
def test_get_components_matching_patterns_no_include_returns_empty(test_db, project, pipeline, pipeline_2):
49+
result = JourneyService.get_components_matching_patterns(str(project.id), [], [])
50+
assert result == []
51+
52+
53+
@pytest.mark.integration
54+
def test_get_components_matching_patterns_wildcard_matches_all(test_db, project, pipeline, pipeline_2):
55+
result = JourneyService.get_components_matching_patterns(str(project.id), ["*"], [])
56+
assert {c.id for c in result} == {pipeline.id, pipeline_2.id}
57+
58+
59+
@pytest.mark.integration
60+
def test_get_components_matching_patterns_exact_key(test_db, project, pipeline, pipeline_2):
61+
result = JourneyService.get_components_matching_patterns(str(project.id), ["P1"], [])
62+
assert len(result) == 1
63+
assert result[0].id == pipeline.id
64+
65+
66+
@pytest.mark.integration
67+
def test_get_components_matching_patterns_exclude_applied(test_db, project, pipeline, pipeline_2):
68+
result = JourneyService.get_components_matching_patterns(str(project.id), ["*"], ["P2"])
69+
assert len(result) == 1
70+
assert result[0].id == pipeline.id
71+
72+
73+
@pytest.mark.integration
74+
def test_get_components_matching_patterns_character_class_matches(test_db, project, pipeline, pipeline_2):
75+
# [pP]* matches both P1 and P2 regardless of case
76+
result = JourneyService.get_components_matching_patterns(str(project.id), ["[pP]*"], [])
77+
assert {c.id for c in result} == {pipeline.id, pipeline_2.id}
78+
79+
80+
@pytest.mark.integration
81+
def test_get_components_matching_patterns_character_class_negation(test_db, project, pipeline, pipeline_2):
82+
# [!pP]* excludes keys starting with p or P — neither P1 nor P2 should match
83+
result = JourneyService.get_components_matching_patterns(str(project.id), ["[!pP]*"], [])
84+
assert result == []
85+
86+
87+
@pytest.mark.integration
88+
def test_get_components_matching_patterns_character_class_exclude(test_db, project, pipeline, pipeline_2):
89+
# include all, but exclude keys starting with p or P
90+
result = JourneyService.get_components_matching_patterns(str(project.id), ["*"], ["[pP]*"])
91+
assert result == []
92+
93+
94+
@pytest.mark.integration
95+
def test_get_components_matching_patterns_character_class_exclude_negation(test_db, project, pipeline, pipeline_2):
96+
# exclude *[!1] matches keys not ending in '1' — P2 is excluded, P1 remains
97+
result = JourneyService.get_components_matching_patterns(str(project.id), ["*"], ["*[!1]"])
98+
assert len(result) == 1
99+
assert result[0].id == pipeline.id
100+
101+
102+
@pytest.mark.integration
103+
def test_get_components_matching_patterns_question_mark_matches_any_single_char(test_db, project, pipeline, pipeline_2):
104+
# P? matches any key of exactly two chars starting with P — both P1 and P2 match
105+
result = JourneyService.get_components_matching_patterns(str(project.id), ["P?"], [])
106+
assert {c.id for c in result} == {pipeline.id, pipeline_2.id}
107+
108+
109+
@pytest.mark.integration
110+
def test_get_components_matching_patterns_question_mark_matches_specific(test_db, project, pipeline, pipeline_2):
111+
# ?1 matches any two-char key ending in '1' — only P1 matches
112+
result = JourneyService.get_components_matching_patterns(str(project.id), ["?1"], [])
113+
assert len(result) == 1
114+
assert result[0].id == pipeline.id
115+
116+
117+
@pytest.mark.integration
118+
def test_get_components_matching_patterns_question_mark_exclude(test_db, project, pipeline, pipeline_2):
119+
# exclude ?1 removes P1, leaving P2
120+
result = JourneyService.get_components_matching_patterns(str(project.id), ["*"], ["?1"])
121+
assert len(result) == 1
122+
assert result[0].id == pipeline_2.id
123+
124+
125+
@pytest.mark.integration
126+
def test_get_components_matching_patterns_only_own_project(test_db, project, pipeline, organization, user):
127+
from common.entities import Pipeline, Project
128+
129+
other_project = Project.create(name="Other", organization=organization, created_by=user)
130+
Pipeline.create(name="Other P1", key="P1", project=other_project, created_by=user)
131+
result = JourneyService.get_components_matching_patterns(str(project.id), ["P1"], [])
132+
assert len(result) == 1
133+
assert result[0].id == pipeline.id
134+
135+
136+
# --- apply_component_patterns ---
137+
138+
139+
@pytest.mark.integration
140+
def test_apply_component_patterns_no_include_clears_edges(test_db, journey, pipeline):
141+
JourneyDagEdge.create(journey=journey, left=None, right=pipeline)
142+
journey.component_include_patterns = None
143+
journey.component_exclude_patterns = None
144+
JourneyService.apply_component_patterns(journey)
145+
assert JourneyDagEdge.select().where(JourneyDagEdge.journey == journey).count() == 0
146+
147+
148+
@pytest.mark.integration
149+
def test_apply_component_patterns_adds_matching_as_root_nodes(test_db, journey, pipeline, pipeline_2):
150+
journey.component_include_patterns = "*"
151+
journey.component_exclude_patterns = None
152+
JourneyService.apply_component_patterns(journey)
153+
edges = list(JourneyDagEdge.select().where(JourneyDagEdge.journey == journey))
154+
assert len(edges) == 2
155+
assert all(e.left_id is None for e in edges)
156+
157+
158+
@pytest.mark.integration
159+
def test_apply_component_patterns_preserves_existing_edges(test_db, journey, pipeline, pipeline_2):
160+
JourneyDagEdge.create(journey=journey, left=pipeline, right=pipeline_2)
161+
journey.component_include_patterns = "*"
162+
journey.component_exclude_patterns = None
163+
JourneyService.apply_component_patterns(journey)
164+
edges = list(JourneyDagEdge.select().where(JourneyDagEdge.journey == journey))
165+
# pipeline is root (no predecessor); pipeline->pipeline_2 edge is restored; pipeline_2 gets no root entry
166+
assert len(edges) == 2
167+
root_edges = [e for e in edges if e.left_id is None]
168+
assert len(root_edges) == 1
169+
assert root_edges[0].right_id == pipeline.id
170+
171+
172+
@pytest.mark.integration
173+
def test_apply_component_patterns_drops_edge_when_component_excluded(test_db, journey, pipeline, pipeline_2):
174+
JourneyDagEdge.create(journey=journey, left=pipeline, right=pipeline_2)
175+
journey.component_include_patterns = "*"
176+
journey.component_exclude_patterns = "P2"
177+
JourneyService.apply_component_patterns(journey)
178+
edges = list(JourneyDagEdge.select().where(JourneyDagEdge.journey == journey))
179+
assert len(edges) == 1
180+
assert edges[0].right_id == pipeline.id
181+
182+
183+
# --- add_component_to_matching_journeys ---
184+
185+
186+
@pytest.mark.integration
187+
def test_add_component_to_matching_journeys_matches(test_db, journey, project, pipeline):
188+
journey.component_include_patterns = "*"
189+
journey.save()
190+
JourneyService.add_component_to_matching_journeys(pipeline)
191+
edges = list(JourneyDagEdge.select().where(JourneyDagEdge.journey == journey))
192+
assert len(edges) == 1
193+
assert edges[0].left_id is None
194+
assert edges[0].right_id == pipeline.id
195+
196+
197+
@pytest.mark.integration
198+
def test_add_component_to_matching_journeys_no_include_skips(test_db, journey, pipeline):
199+
JourneyService.add_component_to_matching_journeys(pipeline)
200+
assert JourneyDagEdge.select().where(JourneyDagEdge.journey == journey).count() == 0
201+
202+
203+
@pytest.mark.integration
204+
def test_add_component_to_matching_journeys_excluded(test_db, journey, pipeline):
205+
journey.component_include_patterns = "*"
206+
journey.component_exclude_patterns = "P1"
207+
journey.save()
208+
JourneyService.add_component_to_matching_journeys(pipeline)
209+
assert JourneyDagEdge.select().where(JourneyDagEdge.journey == journey).count() == 0
210+
211+
212+
@pytest.mark.integration
213+
def test_add_component_to_matching_journeys_only_matching_journey(test_db, journey, journey_2, pipeline):
214+
journey.component_include_patterns = "P1"
215+
journey.save()
216+
journey_2.component_include_patterns = "other-*"
217+
journey_2.save()
218+
JourneyService.add_component_to_matching_journeys(pipeline)
219+
assert JourneyDagEdge.select().where(JourneyDagEdge.journey == journey).count() == 1
220+
assert JourneyDagEdge.select().where(JourneyDagEdge.journey == journey_2).count() == 0
221+
222+
11223
@pytest.mark.integration
12224
def test_get_rules_with_rules_journey_exists(test_db, journey, rule):
13225
rules_page = JourneyService.get_rules_with_rules(journey.id, ListRules())

deploy/docker/observability-be.dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ RUN python3 -O -m pip install --no-deps /tmp/dk --prefix=/dk
3232

3333
FROM ${BASE_IMAGE_URL}python:3.12.11-alpine3.22 AS runtime-image
3434

35-
RUN apk update && apk upgrade && apk add --no-cache librdkafka=2.10.0-r0
35+
RUN apk update && apk upgrade && apk add --no-cache librdkafka=2.10.0-r0 \
36+
&& pip install --no-cache-dir --upgrade pip
3637

3738
# Grab the pre-built app from the build-image. This way we don't have
3839
# excess laying around in the final image.

deploy/docker/observability-ui.dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ COPY observability_ui/ /observability_ui
88
RUN yarn
99
RUN yarn build:ci
1010

11-
FROM ${BASE_IMAGE_URL}nginxinc/nginx-unprivileged:alpine3.22
11+
FROM ${BASE_IMAGE_URL}nginxinc/nginx-unprivileged:alpine3.23
12+
13+
USER root
14+
RUN apk upgrade --no-cache
15+
USER nginx
1216

1317
WORKDIR /observability_ui
1418

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Add component_include_patterns and component_exclude_patterns columns to journey table
3+
"""
4+
5+
from yoyo import step
6+
7+
__depends__ = {"20240723_01_K3c3Q-renaming-the-agent-check-interval-column"}
8+
__transactional__ = False
9+
10+
steps = [
11+
step(
12+
"ALTER TABLE `journey` ADD COLUMN `component_include_patterns` varchar(255) NULL",
13+
"ALTER TABLE `journey` DROP COLUMN `component_include_patterns`",
14+
),
15+
step(
16+
"ALTER TABLE `journey` ADD COLUMN `component_exclude_patterns` varchar(255) NULL",
17+
"ALTER TABLE `journey` DROP COLUMN `component_exclude_patterns`",
18+
),
19+
]

observability_api/endpoints/component_view.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from common.api.base_view import Permission
1212
from common.entities import BaseEntity, Project
13+
from common.entity_services import JourneyService
1314
from observability_api.endpoints.entity_view import BaseEntityView
1415
from observability_api.schemas.component_schemas import ComponentPatchSchema
1516

@@ -55,6 +56,7 @@ def post(self, project_id: UUID) -> Response:
5556
component.created_by = self.user
5657
component.project = self.get_entity_or_fail(Project, Project.id == project_id)
5758
self.save_entity_or_fail(component, force_insert=True)
59+
JourneyService.add_component_to_matching_journeys(component)
5860
return make_response(self.schema().dump(component), HTTPStatus.CREATED)
5961

6062
@classmethod

0 commit comments

Comments
 (0)