Repository: RedHatInsights/insights-host-inventory
Pull request #2311: feat(RHINENG-15447): tags data transformation
Author: login: FabriciaDinizRH
Labels: name: on hold
name: ready for review
description: The PR is ready for review
Created: 2025-02-28T04:12:20Z | Merged: 2025-03-10T20:07:56Z

# Overview

This PR is being created to address [RHINENG-15447](https://issues.redhat.com/browse/RHINENG-15447).

- add a `tags_alt` column to the hosts table
- migrate data from `tags` to `tags_alt`
- alter create/update host functions to also populate `tags_alt`
- update a couple of tests to verify the `tags_alt` behavior is consistent with `tags`

**DO NOT MERGE IT YET EVEN IF APPROVED**

## PR Checklist

- [x] Keep PR title short, ideally under 72 characters
- [ ] Descriptive comments provided in complex code blocks
- [ ] Include raw query examples in the PR description, if adding/modifying SQL query
- [x] Tests: validate optimal/expected output
- [ ] Tests: validate exceptions and failure scenarios
- [ ] Tests: edge cases
- [ ] Recovers or fails gracefully during potential resource outages (e.g. DB, Kafka)
- [ ] Uses [type hinting](https://docs.python.org/3/library/typing.html), if convenient
- [ ] Documentation, if this PR changes the way other services interact with host inventory
- [ ] Links to related PRs

## Secure Coding Practices Documentation Reference

You can find documentation on this checklist [here](https://github.com/RedHatInsights/secure-coding-checklist).

## Secure Coding Checklist

- [ ] Input Validation
- [ ] Output Encoding
- [ ] Authentication and Password Management
- [ ] Session Management
- [ ] Access Control
- [ ] Cryptographic Practices
- [ ] Error Handling and Logging
- [ ] Data Protection
- [ ] Communication Security
- [ ] System Configuration
- [ ] Database Security
- [ ] File Management
- [ ] Memory Management
- [ ] General Coding Practices



Diff:
diff --git a/app/models.py b/app/models.py
index 9d49e5f938..2cf6560025 100644
--- a/app/models.py
+++ b/app/models.py
@@ -45,6 +45,7 @@
 from app.exceptions import InventoryException
 from app.exceptions import ValidationException
 from app.logging import get_logger
+from app.utils import Tag
 from app.validators import check_empty_keys
 from app.validators import verify_ip_address_format
 from app.validators import verify_mac_address_format
@@ -190,11 +191,15 @@ def __init__(
         org_id=None,
         facts=None,
         tags=None,
+        tags_alt=None,
         system_profile_facts=None,
         groups=None,
     ):
         if tags is None:
             tags = {}
+            tags_alt = []
+        else:
+            tags_alt = self._populate_tags_alt_from_tags(tags)
         if groups is None:
             groups = []
 
@@ -210,6 +215,7 @@ def __init__(
         self.org_id = org_id
         self.facts = facts or {}
         self.tags = tags
+        self.tags_alt = tags_alt
         self.system_profile_facts = system_profile_facts or {}
         self.groups = groups or []
 
@@ -218,6 +224,17 @@ def _update_ansible_host(self, ansible_host):
             # Allow a user to clear out the ansible host with an empty string
             self.ansible_host = ansible_host
 
+    def _populate_tags_alt_from_tags(self, tags):
+        if isinstance(tags, dict):
+            transformed_tags_obj = Tag.create_tags_from_nested(tags)
+            transformed_tags = [tag.data() for tag in transformed_tags_obj]
+        elif isinstance(tags, list):
+            transformed_tags = tags
+        else:
+            raise TypeError("Tags must be dict or list")
+
+        return transformed_tags
+
     @hybrid_property
     def operating_system(self):
         # Used when accessing the instance's property
@@ -260,6 +277,7 @@ def operating_system(cls):
     modified_on = db.Column(db.DateTime(timezone=True), default=_time_now, onupdate=_time_now)
     facts = db.Column(JSONB)
     tags = db.Column(JSONB)
+    tags_alt = db.Column(JSONB)
     canonical_facts = db.Column(JSONB)
     system_profile_facts = db.Column(JSONB)
     groups = db.Column(JSONB)
@@ -280,6 +298,7 @@ def __init__(
         org_id=None,
         facts=None,
         tags=None,
+        tags_alt=None,
         system_profile_facts=None,
         stale_timestamp=None,
         reporter=None,
@@ -288,6 +307,7 @@ def __init__(
     ):
         if tags is None:
             tags = {}
+
         if groups is None:
             groups = []
 
@@ -301,7 +321,16 @@ def __init__(
             raise ValidationException("The tags field cannot be null.")
 
         super().__init__(
-            canonical_facts, display_name, ansible_host, account, org_id, facts, tags, system_profile_facts, groups
+            canonical_facts,
+            display_name,
+            ansible_host,
+            account,
+            org_id,
+            facts,
+            tags,
+            tags_alt,
+            system_profile_facts,
+            groups,
         )
 
         # without reporter and stale_timestamp host payload is invalid.
@@ -421,16 +450,48 @@ def _update_tags(self, tags_dict):
             else:
                 self._delete_tags_namespace(namespace)
 
+        self._update_tags_alt(tags_dict)
+
+    # these duplicated methods are going to be removed in a following task
+    def _update_tags_alt(self, tags_dict):
+        for namespace, ns_tags in tags_dict.items():
+            if ns_tags:
+                self._replace_tags_alt_in_namespace(namespace, ns_tags)
+            else:
+                self._delete_tags_alt_namespace(namespace)
+
     def _replace_tags_in_namespace(self, namespace, tags):
         self.tags[namespace] = tags
         orm.attributes.flag_modified(self, "tags")
 
+    def _replace_tags_alt_in_namespace(self, namespace, tags_alt):
+        namespace_found = False
+        for tag in self.tags_alt:
+            if tag.get("namespace") == namespace:
+                namespace_found = True
+                ((key, value),) = tags_alt.items()
+                tag["key"] = key
+                tag["value"] = value[0]
+        if not namespace_found:
+            ((key, value),) = tags_alt.items()
+            self.tags_alt.append({"namespace": namespace, "key": key, "value": value[0]})
+
+        orm.attributes.flag_modified(self, "tags_alt")
+
     def _delete_tags_namespace(self, namespace):
         with suppress(KeyError):
             del self.tags[namespace]
 
         orm.attributes.flag_modified(self, "tags")
 
+    def _delete_tags_alt_namespace(self, namespace):
+        for i, tag in enumerate(self.tags_alt):
+            if tag.get("namespace") == namespace:
+                with suppress(KeyError):
+                    del self.tags_alt[i]
+
+            orm.attributes.flag_modified(self, "tags_alt")
+
     def _cleanup_tags(self):
         namespaces_to_delete = tuple(namespace for namespace, items in self.tags.items() if not items)
         for namespace in namespaces_to_delete:
@@ -779,6 +840,7 @@ class Meta:
     facts = fields.List(fields.Nested(FactsSchema))
     system_profile = fields.Dict()
     tags = fields.Raw()
+    tags_alt = fields.Raw()
     groups = fields.List(fields.Dict())
 
     def __init__(self, system_profile_schema=None, *args, **kwargs):
@@ -838,7 +900,7 @@ def _normalize_system_profile(normalize, data):
         return {**data, "system_profile": system_profile}
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
         return LimitedHost(
             canonical_facts=canonical_facts,
             display_name=data.get("display_name"),
@@ -847,6 +909,7 @@ def build_model(data, canonical_facts, facts, tags):
             org_id=data.get("org_id"),
             facts=facts,
             tags=tags,
+            tags_alt=tags_alt if tags_alt else [],
             system_profile_facts=data.get("system_profile", {}),
             groups=data.get("groups", []),
         )
@@ -881,7 +944,9 @@ class Meta:
     reporter = fields.Str(required=True, validate=marshmallow_validate.Length(min=1, max=255))
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
+        if tags_alt is None:
+            tags_alt = []
         return Host(
             canonical_facts,
             data.get("display_name"),
@@ -890,6 +955,7 @@ def build_model(data, canonical_facts, facts, tags):
             data.get("org_id"),
             facts,
             tags,
+            tags_alt,
             data.get("system_profile", {}),
             data["stale_timestamp"],
             data["reporter"],
diff --git a/app/serialization.py b/app/serialization.py
index 828b70de0e..b45c2af3af 100644
--- a/app/serialization.py
+++ b/app/serialization.py
@@ -94,7 +94,8 @@ def deserialize_host(
     canonical_facts = _deserialize_canonical_facts(validated_data)
     facts = _deserialize_facts(validated_data.get("facts"))
     tags = _deserialize_tags(validated_data.get("tags"))
-    return schema.build_model(validated_data, canonical_facts, facts, tags)
+    tags_alt = validated_data.get("tags_alt", [])
+    return schema.build_model(validated_data, canonical_facts, facts, tags, tags_alt)
 
 
 def deserialize_canonical_facts(raw_data, all=False):
@@ -151,6 +152,7 @@ def serialize_host(
         "created": lambda: _serialize_datetime(host.created_on),
         "updated": lambda: _serialize_datetime(host.modified_on),
         "tags": lambda: _serialize_tags(host.tags),
+        "tags_alt": lambda: host.tags_alt,
         "state": lambda: Conditions.find_host_state(
             stale_timestamp=timestamps["stale_timestamp"],
             stale_warning_timestamp=timestamps["stale_warning_timestamp"],
diff --git a/app/utils.py b/app/utils.py
index 10e76c0023..6828076f96 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -312,6 +312,15 @@ def to_nested(self):
 
         return {self.namespace: {self.key: []}}
 
+    @staticmethod
+    def to_structured(tags_dict_list):
+        structured_tags = []
+        for tag_dict in tags_dict_list:
+            tag = Tag(namespace=tag_dict.get("namespace"), key=tag_dict.get("key"), value=tag_dict.get("value"))
+
+            structured_tags.append(tag)
+        return structured_tags
+
     @staticmethod
     def create_nested_from_tags(tags):
         """
@@ -386,8 +395,8 @@ def create_tags_from_nested(nested_tags):
         tags = []
         for namespace in nested_tags:
             for key in nested_tags[namespace]:
-                if len(nested_tags[namespace][key]) == 0:
-                    tags.append(Tag(Tag.serialize_namespace(namespace), key))
+                if nested_tags[namespace][key] is None or len(nested_tags[namespace][key]) == 0:
+                    tags.append(Tag(Tag.serialize_namespace(namespace), key, None))
                 else:
                     for value in nested_tags[namespace][key]:
                         tags.append(Tag(Tag.serialize_namespace(namespace), key, value))
diff --git a/app_migrations/6f44b7ecd7be.sql b/app_migrations/6f44b7ecd7be.sql
new file mode 100644
index 0000000000..0c752f20ef
--- /dev/null
+++ b/app_migrations/6f44b7ecd7be.sql
@@ -0,0 +1,30 @@
+BEGIN;
+
+-- Running upgrade ecbe7e63f6d9 -> 6f44b7ecd7be
+
+ALTER TABLE hbi.hosts ADD COLUMN tags_alt JSONB;
+
+UPDATE hbi.hosts h
+        SET tags_alt = sub.tags_alt
+        FROM (
+            SELECT id, COALESCE(
+                (SELECT JSONB_AGG(
+                            JSONB_BUILD_OBJECT(
+                                'namespace', ns.namespace,
+                                'key', k.key,
+                                'value', v.value
+                            )
+                        )
+                    FROM JSONB_OBJECT_KEYS(tags) AS ns(namespace),
+                        JSONB_EACH(tags -> ns.namespace) AS k(key, value),
+                        JSONB_ARRAY_ELEMENTS_TEXT(k.value) AS v(value)),
+                '[]'::jsonb
+            ) AS tags_alt
+            FROM hbi.hosts h
+            WHERE (h.system_profile_facts->>'host_type' = 'edge') AND h.tags <> '{}'
+        ) AS sub
+        WHERE h.id = sub.id;;
+
+UPDATE hbi.alembic_version SET version_num='6f44b7ecd7be' WHERE hbi.alembic_version.version_num = 'ecbe7e63f6d9';
+
+COMMIT;
diff --git a/migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py b/migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py
new file mode 100644
index 0000000000..58179a8cac
--- /dev/null
+++ b/migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py
@@ -0,0 +1,48 @@
+"""Add tags_alt column and migrate tags data
+
+Revision ID: 6f44b7ecd7be
+Revises: ecbe7e63f6d9
+Create Date: 2025-02-28 00:46:46.297631
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "6f44b7ecd7be"
+down_revision = "ecbe7e63f6d9"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column("hosts", sa.Column("tags_alt", postgresql.JSONB(astext_type=sa.Text()), nullable=True), schema="hbi")
+
+    op.execute("""
+        UPDATE hbi.hosts h
+        SET tags_alt = sub.tags_alt
+        FROM (
+            SELECT id, COALESCE(
+                (SELECT JSONB_AGG(
+                            JSONB_BUILD_OBJECT(
+                                'namespace', ns.namespace,
+                                'key', k.key,
+                                'value', v.value
+                            )
+                        )
+                    FROM JSONB_OBJECT_KEYS(tags) AS ns(namespace),
+                        JSONB_EACH(tags -> ns.namespace) AS k(key, value),
+                        JSONB_ARRAY_ELEMENTS_TEXT(k.value) AS v(value)),
+                '[]'::jsonb
+            ) AS tags_alt
+            FROM hbi.hosts h
+            WHERE (h.system_profile_facts->>'host_type' = 'edge') AND h.tags <> '{}'
+        ) AS sub
+        WHERE h.id = sub.id;
+    """)
+
+
+def downgrade():
+    op.drop_column("hosts", "tags_alt", schema="hbi")
diff --git a/tests/test_host_mq_service.py b/tests/test_host_mq_service.py
index 7234df9199..8f79efad22 100644
--- a/tests/test_host_mq_service.py
+++ b/tests/test_host_mq_service.py
@@ -22,6 +22,7 @@
 from app.queue.host_mq import handle_message
 from app.queue.host_mq import update_system_profile
 from app.queue.host_mq import write_add_update_event_message
+from app.utils import Tag
 from lib.host_repository import AddHostResult
 from tests.helpers.db_utils import create_reference_host_in_db
 from tests.helpers.mq_utils import FakeMessage
@@ -1124,8 +1125,12 @@ def test_replace_tags_of_host_by_list(mq_create_or_update_host, db_get_host_by_i
             mq_create_or_update_host(host)
 
             record = db_get_host_by_insights_id(insights_id)
+            structured_tags = structured_tags = [
+                Tag.from_nested({ns: ns_item}) for ns, ns_item in expected_tags.items()
+            ]
 
             assert expected_tags == record.tags
+            assert Tag.create_flat_tags_from_structured(structured_tags) == record.tags_alt
 
 
 def test_replace_host_tags_by_dict(mq_create_or_update_host, db_get_host_by_insights_id, subtests):
@@ -1168,8 +1173,10 @@ def test_replace_host_tags_by_dict(mq_create_or_update_host, db_get_host_by_insi
             mq_create_or_update_host(host)
 
             record = db_get_host_by_insights_id(insights_id)
+            structured_tags = [Tag.from_nested({ns: ns_item}) for ns, ns_item in expected_tags.items()]
 
             assert expected_tags == record.tags
+            assert Tag.create_flat_tags_from_structured(structured_tags) == record.tags_alt
 
 
 def test_keep_host_tags_by_empty(mq_create_or_update_host, db_get_host_by_insights_id, subtests):
@@ -1201,6 +1208,7 @@ def test_add_host_default_empty_dict(tags, mq_create_or_update_host, db_get_host
     record = db_get_host_by_insights_id(insights_id)
 
     assert record.tags == {}
+    assert record.tags_alt == []
 
 
 def test_delete_host_tags(mq_create_or_update_host, db_get_host_by_insights_id, subtests):
@@ -1236,8 +1244,10 @@ def test_delete_host_tags(mq_create_or_update_host, db_get_host_by_insights_id,
             mq_create_or_update_host(host)
 
             record = db_get_host_by_insights_id(insights_id)
+            structured_tags = [Tag.from_nested({ns: ns_item}) for ns, ns_item in expected_tags.items()]
 
             assert expected_tags == record.tags
+            assert Tag.create_flat_tags_from_structured(structured_tags) == record.tags_alt
 
 
 @pytest.mark.usefixtures("event_datetime_mock")
diff --git a/tests/test_models.py b/tests/test_models.py
index ab129a9292..76c9f55044 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -267,19 +267,23 @@ def test_create_host_with_tags(tags, db_create_host):
     )
 
     assert created_host.tags == tags
+    assert created_host.tags_alt == tags
 
 
 def test_update_host_with_tags(db_create_host):
     insights_id = str(uuid.uuid4())
     old_tags = Tag("Sat", "env", "prod").to_nested()
+    old_tags_alt = Tag.create_flat_tags_from_structured([Tag("Sat", "env", "prod")])
     existing_host = db_create_host(
         extra_data={"canonical_facts": {"insights_id": insights_id}, "display_name": "tagged", "tags": old_tags}
     )
 
     assert existing_host.tags == old_tags
+    assert existing_host.tags_alt == old_tags_alt
 
     # On update each namespace in the input host's tags should be updated.
     new_tags = Tag.create_nested_from_tags([Tag("Sat", "env", "ci"), Tag("AWS", "env", "prod")])
+    new_tags_alt = Tag.create_flat_tags_from_structured([Tag("Sat", "env", "ci"), Tag("AWS", "env", "prod")])
     input_host = db_create_host(
         extra_data={"canonical_facts": {"insights_id": insights_id}, "display_name": "tagged", "tags": new_tags}
     )
@@ -287,6 +291,7 @@ def test_update_host_with_tags(db_create_host):
     existing_host.update(input_host)
 
     assert existing_host.tags == new_tags
+    assert existing_host.tags_alt == new_tags_alt
 
 
 def test_update_host_with_no_tags(db_create_host):
diff --git a/tests/test_unit.py b/tests/test_unit.py
index 5b6766c498..96e496758d 100755
--- a/tests/test_unit.py
+++ b/tests/test_unit.py
@@ -1132,6 +1132,7 @@ def test_with_all_fields(self, deserialize_canonical_facts, deserialize_facts, d
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             host_input["system_profile"],
             host_input["stale_timestamp"],
             host_input["reporter"],
@@ -1191,6 +1192,7 @@ def test_without_facts(self, deserialize_canonical_facts, deserialize_facts, des
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             host_input["system_profile"],
             host_input["stale_timestamp"],
             host_input["reporter"],
@@ -1250,6 +1252,7 @@ def test_without_tags(self, deserialize_canonical_facts, deserialize_facts, dese
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             host_input["system_profile"],
             host_input["stale_timestamp"],
             host_input["reporter"],
@@ -1312,6 +1315,7 @@ def test_without_display_name(self, deserialize_canonical_facts, deserialize_fac
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             host_input["system_profile"],
             host_input["stale_timestamp"],
             host_input["reporter"],
@@ -1369,6 +1373,7 @@ def test_without_system_profile(self, deserialize_canonical_facts, deserialize_f
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             {},
             host_input["stale_timestamp"],
             host_input["reporter"],
@@ -1414,6 +1419,7 @@ def test_without_groups(self, deserialize_canonical_facts, deserialize_facts, de
             host_input["org_id"],
             deserialize_facts.return_value,
             deserialize_tags.return_value,
+            [],
             host_input["system_profile"],
             host_input["stale_timestamp"],
             host_input["reporter"],


Reviews:
user: login: strider
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T09:51:23Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:11:42Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:47:11Z
user: login: kruai
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:12:29Z
user: login: kruai
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:17:16Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T04:44:34Z
user: login: strider
body: LGTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T08:12:25Z
user: login: chambridge
body: A couple code questions and one adjustment around what the migration is attempting to update.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:41:49Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T01:06:57Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T01:11:03Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T01:12:09Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T01:18:11Z
user: login: chambridge
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:56:56Z
user: login: chambridge
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T13:59:14Z
user: login: chambridge
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T14:03:42Z
user: login: kruai
body: Just a couple of small suggestions
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T13:41:29Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:23:16Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:36:40Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:39:03Z
user: login: chambridge
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:10:57Z
user: login: thearifismail
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T18:15:06Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:03:32Z
user: login: kruai
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-10T19:38:29Z

Review comments:
diff_hunk: @@ -430,6 +447,20 @@ def _delete_tags_namespace(self, namespace):
 
         orm.attributes.flag_modified(self, "tags")
 
+    def _populate_tags_alt_from_tags(self, tags):
+        transformed_tags = []
+
+        if type(tags) is dict:
path: app/models.py
user: login: strider
body: Why not using `isinstance` built-in function here like you do in the `_deserialize_tags_alt` function below ?
created_at: 2025-03-03T09:51:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -430,6 +447,20 @@ def _delete_tags_namespace(self, namespace):
 
         orm.attributes.flag_modified(self, "tags")
 
+    def _populate_tags_alt_from_tags(self, tags):
+        transformed_tags = []
+
+        if type(tags) is dict:
path: app/models.py
user: login: FabriciaDinizRH
body: no particular reason, changing it

created_at: 2025-03-03T13:11:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -357,3 +358,17 @@ def test_get_tags_with_dollar_signs_via_db(api_get, db_create_host, tag, flatten
 
     assert response_status == 200
     assert flattened_tag == response_data["results"][0]["tag"]
+
+
+def test_tag_alt_is_created(api_get, db_create_host):
+    tag = {"insights-client": {"test-key": "test-value"}}
+    flattened_tag = {"namespace": "insights-client", "key": "test-key", "value": "test-value"}
+    db_create_host(extra_data={"tags": tag})
+
+    url = build_tags_url()
path: tests/test_tags.py
user: login: FabriciaDinizRH
body: I'm aware I'm using the wrong url function here, I'll commit the fixed version when the test passes
created_at: 2025-03-03T13:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -430,6 +447,20 @@ def _delete_tags_namespace(self, namespace):
 
         orm.attributes.flag_modified(self, "tags")
 
+    def _populate_tags_alt_from_tags(self, tags):
+        transformed_tags = []
+
+        if type(tags) is dict:
+            transformed_tags_obj = Tag.create_tags_from_nested(tags)
+        elif type(tags) is list:
+            transformed_tags_obj = Tag.to_structured(tags)
path: app/models.py
user: login: kruai
body: If the tags are of type "list", they're already in the correct format, right? seems unnecessary to convert them to structured and then back to flat afterwards
created_at: 2025-03-03T14:12:29Z
author_association: COLLABORATOR
diff_hunk: @@ -189,11 +190,13 @@ def __init__(
         org_id=None,
         facts=None,
         tags=None,
+        tags_alt=None,
         system_profile_facts=None,
         groups=None,
     ):
         if tags is None:
             tags = {}
+            tags_alt = []
path: app/models.py
user: login: kruai
body: What about if `tags` is populated, but `tags_alt` is None?
created_at: 2025-03-03T14:17:16Z
author_association: COLLABORATOR
diff_hunk: @@ -189,11 +190,13 @@ def __init__(
         org_id=None,
         facts=None,
         tags=None,
+        tags_alt=None,
         system_profile_facts=None,
         groups=None,
     ):
         if tags is None:
             tags = {}
+            tags_alt = []
path: app/models.py
user: login: FabriciaDinizRH
body: good catch
created_at: 2025-03-05T04:44:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,6 +293,11 @@ def __init__(
     ):
         if tags is None:
             tags = {}
+            if tags_alt is None:
+                tags_alt = []
path: app/models.py
user: login: chambridge
body: Is this if condition necessary or shouldn't we always just set `tags_alt` to `[]` if we are setting `tags` to `{}`
created_at: 2025-03-05T13:30:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,6 +293,11 @@ def __init__(
     ):
         if tags is None:
             tags = {}
+            if tags_alt is None:
+                tags_alt = []
+        elif tags_alt is None:
path: app/models.py
user: login: chambridge
body: Do we need this to be an `elif` and check for `None`, instead of just an `else`?
created_at: 2025-03-05T13:31:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -836,7 +897,7 @@ def _normalize_system_profile(normalize, data):
         return {**data, "system_profile": system_profile}
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
path: app/models.py
user: login: chambridge
body: Why not set the default for `tags_alt` to `[]`?
created_at: 2025-03-05T13:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,7 +941,9 @@ class Meta:
     reporter = fields.Str(required=True, validate=marshmallow_validate.Length(min=1, max=255))
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
path: app/models.py
user: login: chambridge
body: Why not set the default for `tags_alt` to `[]`?
created_at: 2025-03-05T13:34:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+"""Add tags_alt column and migrate tags data
+
+Revision ID: 6f44b7ecd7be
+Revises: ecbe7e63f6d9
+Create Date: 2025-02-28 00:46:46.297631
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "6f44b7ecd7be"
+down_revision = "ecbe7e63f6d9"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column("hosts", sa.Column("tags_alt", postgresql.JSONB(astext_type=sa.Text()), nullable=True), schema="hbi")
+
+    op.execute("""
+        UPDATE hbi.hosts h
+        SET tags_alt = sub.tags_alt
+        FROM (
+            SELECT id, COALESCE(
+                (SELECT JSONB_AGG(
+                            JSONB_BUILD_OBJECT(
+                                'namespace', ns.namespace,
+                                'key', k.key,
+                                'value', v.value
+                            )
+                        )
+                    FROM JSONB_OBJECT_KEYS(tags) AS ns(namespace),
+                        JSONB_EACH(tags -> ns.namespace) AS k(key, value),
+                        JSONB_ARRAY_ELEMENTS_TEXT(k.value) AS v(value)),
+                '[]'::jsonb
+            ) AS tags_alt
+            FROM hbi.hosts
+        ) AS sub
+        WHERE h.id = sub.id;
path: migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py
user: login: chambridge
body: So this migration would run against every host in the hosts table. We should be able to filter out the UPDATE to run against only hosts that have tag data and I believe during our standup we discussed only updating those hosts that are of host_type edge, assuming the rest would get updated via check-ins to minimize the impact of the migration.
created_at: 2025-03-05T13:38:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +267,7 @@ def test_create_host_with_tags(tags, db_create_host):
     )
 
     assert created_host.tags == tags
+    assert created_host.tags_alt == tags
path: tests/test_models.py
user: login: chambridge
body: Is this an accurate assertion? Shouldn't these be different formats?
created_at: 2025-03-05T13:40:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,6 +293,11 @@ def __init__(
     ):
         if tags is None:
             tags = {}
+            if tags_alt is None:
+                tags_alt = []
path: app/models.py
user: login: FabriciaDinizRH
body: hm, makes sense for them to match

created_at: 2025-03-06T01:06:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+"""Add tags_alt column and migrate tags data
+
+Revision ID: 6f44b7ecd7be
+Revises: ecbe7e63f6d9
+Create Date: 2025-02-28 00:46:46.297631
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "6f44b7ecd7be"
+down_revision = "ecbe7e63f6d9"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column("hosts", sa.Column("tags_alt", postgresql.JSONB(astext_type=sa.Text()), nullable=True), schema="hbi")
+
+    op.execute("""
+        UPDATE hbi.hosts h
+        SET tags_alt = sub.tags_alt
+        FROM (
+            SELECT id, COALESCE(
+                (SELECT JSONB_AGG(
+                            JSONB_BUILD_OBJECT(
+                                'namespace', ns.namespace,
+                                'key', k.key,
+                                'value', v.value
+                            )
+                        )
+                    FROM JSONB_OBJECT_KEYS(tags) AS ns(namespace),
+                        JSONB_EACH(tags -> ns.namespace) AS k(key, value),
+                        JSONB_ARRAY_ELEMENTS_TEXT(k.value) AS v(value)),
+                '[]'::jsonb
+            ) AS tags_alt
+            FROM hbi.hosts
+        ) AS sub
+        WHERE h.id = sub.id;
path: migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py
user: login: FabriciaDinizRH
body: I saw that coalesce was supposed to do this filtering of only returning hosts that had the tags column, is the `where` messing it up?
created_at: 2025-03-06T01:11:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +267,7 @@ def test_create_host_with_tags(tags, db_create_host):
     )
 
     assert created_host.tags == tags
+    assert created_host.tags_alt == tags
path: tests/test_models.py
user: login: FabriciaDinizRH
body: sometimes they aren't, at least in a couple of tests the tags are in list format
created_at: 2025-03-06T01:12:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,7 +941,9 @@ class Meta:
     reporter = fields.Str(required=True, validate=marshmallow_validate.Length(min=1, max=255))
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
path: app/models.py
user: login: FabriciaDinizRH
body: ruff complains about it. `B006 Do not use mutable data structures for argument defaults`.
`help: Replace with `None`; initialize within function`
created_at: 2025-03-06T01:18:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -879,7 +941,9 @@ class Meta:
     reporter = fields.Str(required=True, validate=marshmallow_validate.Length(min=1, max=255))
 
     @staticmethod
-    def build_model(data, canonical_facts, facts, tags):
+    def build_model(data, canonical_facts, facts, tags, tags_alt=None):
path: app/models.py
user: login: chambridge
body: I see.
created_at: 2025-03-06T13:56:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+"""Add tags_alt column and migrate tags data
+
+Revision ID: 6f44b7ecd7be
+Revises: ecbe7e63f6d9
+Create Date: 2025-02-28 00:46:46.297631
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "6f44b7ecd7be"
+down_revision = "ecbe7e63f6d9"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column("hosts", sa.Column("tags_alt", postgresql.JSONB(astext_type=sa.Text()), nullable=True), schema="hbi")
+
+    op.execute("""
+        UPDATE hbi.hosts h
+        SET tags_alt = sub.tags_alt
+        FROM (
+            SELECT id, COALESCE(
+                (SELECT JSONB_AGG(
+                            JSONB_BUILD_OBJECT(
+                                'namespace', ns.namespace,
+                                'key', k.key,
+                                'value', v.value
+                            )
+                        )
+                    FROM JSONB_OBJECT_KEYS(tags) AS ns(namespace),
+                        JSONB_EACH(tags -> ns.namespace) AS k(key, value),
+                        JSONB_ARRAY_ELEMENTS_TEXT(k.value) AS v(value)),
+                '[]'::jsonb
+            ) AS tags_alt
+            FROM hbi.hosts
+        ) AS sub
+        WHERE h.id = sub.id;
path: migrations/versions/6f44b7ecd7be_add_tags_alt_column_and_migrate_tags_.py
user: login: chambridge
body: The above will execute on all host records. There is nothing in the where clauses to filter down to hosts that have tags content (meaning more than `{}`) and there is nothing to filter this down based on host type either.
created_at: 2025-03-06T13:59:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -267,6 +267,7 @@ def test_create_host_with_tags(tags, db_create_host):
     )
 
     assert created_host.tags == tags
+    assert created_host.tags_alt == tags
path: tests/test_models.py
user: login: chambridge
body: Oh, I actually think its because the test is written badly to begin with here as the input tags value seen above on lines 252-257. The format here is in the list structure which isn't the format tags would normally be stored in.
created_at: 2025-03-06T14:03:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+Unable to configure watchtower logging.  Please verify watchtower logging configuration!
path: app_migrations/6f44b7ecd7be.sql
user: login: thearifismail
body: This looks like a console output.  Should it be checked into the repo?
created_at: 2025-03-07T14:01:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,6 +312,15 @@ def to_nested(self):
 
         return {self.namespace: {self.key: []}}
 
+    @staticmethod
+    def to_structured(tags_dict_list):
path: app/utils.py
user: login: thearifismail
body: This function is not called from within the host-inventory code in this project.  Is it a callback function that is required at the time of execution?
created_at: 2025-03-07T14:11:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+Unable to configure watchtower logging.  Please verify watchtower logging configuration!
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Context impl PostgresqlImpl.
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Generating static SQL
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Will assume transactional DDL.
path: app_migrations/6f44b7ecd7be.sql
user: login: kruai
body: Why are all these logs here - was that a side effect of the schema dump?
created_at: 2025-03-10T13:32:00Z
author_association: COLLABORATOR
diff_hunk: @@ -288,6 +307,10 @@ def __init__(
     ):
         if tags is None:
             tags = {}
+            tags_alt = []
+        else:
+            tags_alt = super()._populate_tags_alt_from_tags(tags)
+
path: app/models.py
user: login: kruai
body: This check is automatically handled by the parent class LimitedHost, so this shouldn't be needed
created_at: 2025-03-10T13:40:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,73 @@
+Unable to configure watchtower logging.  Please verify watchtower logging configuration!
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Context impl PostgresqlImpl.
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Generating static SQL
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Will assume transactional DDL.
path: app_migrations/6f44b7ecd7be.sql
user: login: FabriciaDinizRH
body: side effect of `gen_offline_sql`
created_at: 2025-03-10T16:23:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+Unable to configure watchtower logging.  Please verify watchtower logging configuration!
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Context impl PostgresqlImpl.
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Generating static SQL
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Will assume transactional DDL.
path: app_migrations/6f44b7ecd7be.sql
user: login: FabriciaDinizRH
body: "You will need to clean up this file to remove the log entries and the update to the alembic version table."
Guess I skipped this line on the README
created_at: 2025-03-10T16:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+Unable to configure watchtower logging.  Please verify watchtower logging configuration!
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Insights Host Inventory Configuration:
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,463] [2887625] [140501292804480] [inventory.app.config] [INFO] Build Version: Unknown
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Host: localhost
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Name: insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:51,464] [2887625] [140501292804480] [inventory.app.config] [INFO] DB Connection URI: postgresql://xxxx:XXXX@localhost:5432/insights
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,030] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /api/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.app] [INFO] Listening on API: /r/insights/platform/inventory/v1
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Initializing Cache
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache using config={'CACHE_TYPE': 'NullCache', 'CACHE_DEFAULT_TIMEOUT': 0}
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,166] [2887625] [140501292804480] [inventory.cache] [INFO] Cache initialized with app.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,168] [2887625] [140501292804480] [inventory.app] [WARNING] Unleash is bypassed by config value. Feature flag toggles will default to their fallback values.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based event notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: The event producer has been disabled.  The message queue based notifications have been disabled.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app] [WARNING] WARNING: Using the NullProducer for the payload tracker producer.  No payload tracker events will be sent to to payload tracker.
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,207] [2887625] [140501292804480] [inventory.app.payload_tracker] [INFO] Using injected producer object (<app.payload_tracker.NullProducer object at 0x7fc8eebee0d0>) for PayloadTracker
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Initializing Segmentio
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,208] [2887625] [140501292804480] [inventory.app] [INFO] Registering Segmentio flush on shutdown
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Context impl PostgresqlImpl.
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Generating static SQL
+[2025-03-07 03:45:53,212] [2887625] [140501292804480] [alembic.runtime.migration] [INFO] Will assume transactional DDL.
path: app_migrations/6f44b7ecd7be.sql
user: login: FabriciaDinizRH
body: fixed

created_at: 2025-03-10T16:39:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,6 +312,15 @@ def to_nested(self):
 
         return {self.namespace: {self.key: []}}
 
+    @staticmethod
+    def to_structured(tags_dict_list):
path: app/utils.py
user: login: FabriciaDinizRH
body: Last I checked I was being used, but apparently I removed that code. I'll open a following PR to removed it
created_at: 2025-03-10T19:03:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: fstavela
created_at: 2025-03-07T11:21:25Z
body: /retest
author_association: CONTRIBUTOR
user: login: FabriciaDinizRH
created_at: 2025-03-07T13:55:36Z
body: /retest
author_association: CONTRIBUTOR
user: login: FabriciaDinizRH
created_at: 2025-03-10T03:03:01Z
body: /retest
author_association: CONTRIBUTOR