Skip to content

Best practices#8

Closed
Daniel36x wants to merge 5 commits intomainfrom
best-practices
Closed

Best practices#8
Daniel36x wants to merge 5 commits intomainfrom
best-practices

Conversation

@Daniel36x
Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions
Copy link
Copy Markdown

🤖 Auditoría de Código y Arquitectura (Gemini AI)

📊 Resumen de la Auditoría

La auditoría del Pull Request revela violaciones significativas a los principios de diseño Screenplay para tareas de Kafka y a las mejores prácticas generales de desarrollo. La tarea PublishMessage acopla directamente la lógica de inicialización del cliente Kafka, omite la gestión de headers de mensajes y utiliza System.out.println para logging, lo cual es inaceptable en entornos de producción. Estos hallazgos impactan negativamente la mantenibilidad, escalabilidad y robustez del framework de pruebas, dificultando la ejecución en paralelo y el aislamiento de pruebas.

🛑 Hallazgos Detallados

📁 src/main/java/co/com/fe/api/tasks/PublishMessage.java

  • Gravedad: 🔴 Crítica
  • Regla Violada: screenplay-kafka-tasks.mdc - Regla 1: Cero Lógica de Conexión en las Tareas.
  • Diagnóstico Técnico: La tarea PublishMessage inicializa y gestiona directamente el KafkaClient a través de un método estático (KafkaClient.getInstance()). Esto viola el principio Screenplay de que las Tasks deben describir qué hace el actor, no cómo interactúa con los sistemas subyacentes. La lógica de conexión y gestión del KafkaProducer debe encapsularse en una Ability asignada al Actor, permitiendo una inyección de dependencias limpia y una configuración flexible.
  • Refactorización Propuesta:
    // src/main/java/co/com/fe/api/abilities/ProduceKafkaEvents.java (Nuevo archivo)
    package co.com.fe.api.abilities;
    
    import net.serenitybdd.screenplay.Ability;
    import net.serenitybdd.screenplay.Actor;
    import org.apache.kafka.clients.producer.KafkaProducer;
    import org.apache.kafka.clients.producer.ProducerRecord;
    import org.apache.kafka.clients.producer.RecordMetadata;
    import org.apache.kafka.common.header.Headers;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.Properties;
    import java.util.concurrent.Future;
    
    public class ProduceKafkaEvents implements Ability {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(ProduceKafkaEvents.class);
        private final KafkaProducer<String, String> producer;
    
        private ProduceKafkaEvents(Properties producerProps) {
            this.producer = new KafkaProducer<>(producerProps);
            LOGGER.info("Kafka Producer initialized with properties: {}", producerProps);
        }
    
        public static ProduceKafkaEvents with(Properties producerProps) {
            return new ProduceKafkaEvents(producerProps);
        }
    
        public static ProduceKafkaEvents as(Actor actor) {
            if (actor.abilityTo(ProduceKafkaEvents.class) == null) {
                throw new IllegalStateException(actor.getName() + " cannot produce Kafka events. They need to be given the ability to ProduceKafkaEvents.");
            }
            return actor.abilityTo(ProduceKafkaEvents.class);
        }
    
        public Future<RecordMetadata> send(String topic, String key, String payload, Headers headers) {
            ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, payload);
            if (headers != null) {
                headers.forEach(header -> record.headers().add(header));
            }
            LOGGER.debug("Sending message to topic: {}, key: {}", topic, key);
            return producer.send(record);
        }
    
        public void close() {
            if (producer != null) {
                producer.close();
                LOGGER.info("Kafka Producer closed.");
            }
        }
    }
    // src/main/java/co/com/fe/api/tasks/PublishMessage.java (Archivo modificado)
    package co.com.fe.api.tasks;
    
    import co.com.fe.api.abilities.ProduceKafkaEvents;
    import net.serenitybdd.screenplay.Actor;
    import net.serenitybdd.screenplay.Task;
    import net.serenitybdd.screenplay.annotations.Subject;
    import org.apache.kafka.clients.producer.RecordMetadata;
    import org.apache.kafka.common.header.Headers;
    import org.apache.kafka.common.header.internals.RecordHeaders; // Para inicializar Headers vacíos
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.concurrent.Future;
    
    import static net.serenitybdd.screenplay.Tasks.instrumented;
    
    /**
     * Task para publicar mensajes en un topic de Kafka
     * Sigue el patrón Screenplay donde las tareas describen QUÉ hace el actor
     */
    public class PublishMessage implements Task {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(PublishMessage.class);
    
        private final String topic;
        private final String key;
        private final String message;
        private final Headers headers; // Nuevo campo para los headers
    
        public PublishMessage(String topic, String key, String message, Headers headers) {
            this.topic = topic;
            this.key = key;
            this.message = message;
            this.headers = headers;
        }
    
        /**
         * Método estático para crear la tarea de forma fluida
         */
        public static PublishMessage to(String topic) {
            return instrumented(PublishMessage.class, topic, null, null, new RecordHeaders()); // Inicializa headers vacíos
        }
    
        /**
         * Especifica la clave del mensaje
         */
        public PublishMessage withKey(String key) {
            return instrumented(PublishMessage.class, this.topic, key, this.message, this.headers);
        }
    
        /**
         * Especifica el contenido del mensaje
         */
        public PublishMessage withPayload(String payload) {
            return instrumented(PublishMessage.class, this.topic, this.key, payload, this.headers);
        }
    
        /**
         * Especifica los headers del mensaje
         */
        public PublishMessage withHeaders(Headers headers) {
            return instrumented(PublishMessage.class, this.topic, this.key, this.message, headers);
        }
    
        /**
         * Ejecuta la tarea de publicar el mensaje
         */
        @Override
        @Subject("publishes message to topic {0}")
        public <T extends Actor> void performAs(T actor) {
            // El actor debe tener la habilidad de producir eventos Kafka
            ProduceKafkaEvents kafkaProducerAbility = ProduceKafkaEvents.as(actor);
    
            try {
                // Enviar el mensaje y esperar confirmación
                Future<RecordMetadata> future = kafkaProducerAbility.send(topic, key, message, headers);
                RecordMetadata metadata = future.get(); // Espera síncrona para asegurar entrega
                LOGGER.info("Published -> topic:{} partition:{} offset:{} timestamp:{}",
                        metadata.topic(), metadata.partition(), metadata.offset(), metadata.timestamp());
    
            } catch (Exception e) {
                LOGGER.error("Failed to publish message to topic: {}", topic, e);
                throw new RuntimeException("Failed to publish message to topic: " + topic, e);
            }
        }
    }

📁 src/main/java/co/com/fe/api/tasks/PublishMessage.java

  • Gravedad: 🟠 Advertencia
  • Regla Violada: screenplay-kafka-tasks.mdc - Regla 2: Headers y Claves (Keys).
  • Diagnóstico Técnico: La implementación original no provee un mecanismo explícito para añadir headers a los mensajes de Kafka. Los headers son cruciales para la trazabilidad (traceId), el enrutamiento y la contextualización de eventos en arquitecturas orientadas a eventos. La regla exige que las tareas aseguren la inclusión de headers necesarios.
  • Refactorización Propuesta: (Incluida en la refactorización anterior)
    Se ha añadido un campo headers al constructor y un método withHeaders(Headers headers) para permitir la inyección de headers de forma fluida. La Ability ProduceKafkaEvents ahora acepta y aplica estos headers al ProducerRecord.

📁 src/main/java/co/com/fe/api/tasks/PublishMessage.java

  • Gravedad: 🔴 Crítica
  • Regla Violada: Mejores Prácticas Generales - 1. Logs en Producción.
  • Diagnóstico Técnico: El código utiliza System.out.println para registrar la información de publicación del mensaje. Esta práctica está prohibida en código de producción y frameworks de pruebas, ya que carece de control de niveles de log, formato, destino y rendimiento. Se debe utilizar una librería de logging estándar como SLF4J/Logback.
  • Refactorización Propuesta: (Incluida en la refactorización anterior)
    Se ha reemplazado System.out.println por LOGGER.info() y LOGGER.error() utilizando org.slf4j.Logger.

📁 src/main/java/co/com/fe/api/tasks/PublishMessage.java

  • Gravedad: 🟠 Advertencia
  • Regla Violada: Mejores Prácticas Generales - 2. Bloques Catch vacíos o malos manejos de excepciones.
  • Diagnóstico Técnico: Aunque la excepción se relanza, el bloque catch no registra el error de manera adecuada antes de re-lanzar. Es fundamental que cualquier excepción capturada sea loggeada con el nivel de severidad apropiado (e.g., ERROR) para facilitar la depuración y el monitoreo.
  • Refactorización Propuesta: (Incluida en la refactorización anterior)
    Se ha añadido LOGGER.error("Failed to publish message to topic: {}", topic, e); dentro del bloque catch para asegurar que el error sea registrado correctamente antes de ser re-lanzado.

@github-actions
Copy link
Copy Markdown

🤖 Auditoría de Código y Arquitectura (Gemini AI)

El Pull Request presenta una implementación inicial para la publicación de mensajes en Kafka, pero adolece de varias violaciones arquitectónicas críticas y de buenas prácticas que comprometen la mantenibilidad, escalabilidad y robustez del framework de pruebas. La principal preocupación radica en la gestión del KafkaProducer directamente dentro de la Task, lo cual contraviene el patrón Screenplay y las reglas de inyección de dependencias para habilidades. Adicionalmente, el uso de System.out.println es una práctica prohibida en entornos de pruebas automatizadas.

📁 src/main/java/co/com/fe/api/tasks/PublishMessage.java

  • Gravedad: 🔴 Crítica

  • Regla Violada: screenplay-kafka-tasks.mdc (Regla 1: Cero Lógica de Conexión en las Tareas), screenplay-kafka-tasks.mdc (Regla 2: Headers y Claves), Mejores Prácticas Generales (Regla 1: Logs en Producción)

  • Diagnóstico Técnico:

    1. Violación de Screenplay (Habilidades): La Task PublishMessage inicializa o recupera directamente el KafkaClient (KafkaClient.getInstance()). Esto es una violación directa del patrón Screenplay, donde la gestión de recursos externos (como un KafkaProducer) debe encapsularse en una Ability que el Actor can() realizar. Las Tasks deben describir qué hacer, no cómo gestionar la conexión.
    2. Falta de Manejo de Headers: La implementación actual no provee un mecanismo para añadir Headers a los mensajes de Kafka, lo cual es fundamental para la trazabilidad (ej. traceId, source) y el aislamiento de pruebas en arquitecturas orientadas a eventos. La regla exige asegurar la inclusión de headers necesarios.
    3. Uso de System.out.println: Se utiliza System.out.println para registrar la información de publicación. Esta práctica está prohibida en favor de un sistema de logging estructurado como SLF4J/Logback, que permite un control granular sobre los niveles de log y la salida.
  • Refactorización Propuesta:
    Se propone la creación de una Ability (ProduceKafkaEvents) para encapsular la lógica del KafkaProducer y su configuración. La Task PublishMessage delegará la acción de envío a esta Ability. Además, se añade soporte para headers y se reemplaza System.out.println por org.slf4j.Logger.

    Paso 1: Crear la Habilidad ProduceKafkaEvents

    package co.com.fe.api.abilities;
    
    import net.serenitybdd.screenplay.Ability;
    import net.serenitybdd.screenplay.Actor;
    import org.apache.kafka.clients.producer.KafkaProducer;
    import org.apache.kafka.clients.producer.ProducerRecord;
    import org.apache.kafka.clients.producer.RecordMetadata;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.Map;
    import java.util.Properties;
    import java.util.concurrent.Future;
    
    /**
     * Habilidad para que un Actor pueda producir eventos en Kafka.
     * Encapsula la inicialización y gestión del KafkaProducer.
     */
    public class ProduceKafkaEvents implements Ability {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(ProduceKafkaEvents.class);
        private final KafkaProducer<String, String> producer;
    
        // Constructor privado para forzar el uso del método factoría
        private ProduceKafkaEvents(Properties producerProperties) {
            this.producer = new KafkaProducer<>(producerProperties);
            LOGGER.info("KafkaProducer inicializado con propiedades: {}", producerProperties);
        }
    
        /**
         * Crea una instancia de la habilidad con las propiedades del productor.
         * Las propiedades deben ser cargadas desde serenity.conf o un archivo de configuración.
         */
        public static ProduceKafkaEvents with(Properties producerProperties) {
            return new ProduceKafkaEvents(producerProperties);
        }
    
        /**
         * Recupera la habilidad de un Actor.
         */
        public static ProduceKafkaEvents as(Actor actor) {
            return actor.abilityTo(ProduceKafkaEvents.class);
        }
    
        /**
         * Envía un mensaje a un tópico de Kafka.
         *
         * @param topic   El nombre del tópico.
         * @param key     La clave del mensaje (para particionamiento).
         * @param payload El contenido del mensaje.
         * @param headers Un mapa de headers opcionales para el mensaje.
         * @return Un Future que representa el resultado del envío.
         */
        public Future<RecordMetadata> send(String topic, String key, String payload, Map<String, String> headers) {
            ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, payload);
            if (headers != null) {
                headers.forEach((headerKey, headerValue) ->
                    record.headers().add(headerKey, headerValue.getBytes())
                );
            }
            LOGGER.debug("Enviando mensaje Kafka a tópico: {}, clave: {}, payload: {}", topic, key, payload);
            return producer.send(record);
        }
    
        /**
         * Cierra el productor de Kafka. Debería ser llamado en un hook de limpieza si es necesario.
         */
        public void close() {
            if (producer != null) {
                producer.close();
                LOGGER.info("KafkaProducer cerrado.");
            }
        }
    }

    Paso 2: Modificar la Task PublishMessage

    package co.com.fe.api.tasks;
    
    import co.com.fe.api.abilities.ProduceKafkaEvents; // Importar la nueva habilidad
    import net.serenitybdd.screenplay.Actor;
    import net.serenitybdd.screenplay.Task;
    import net.serenitybdd.screenplay.annotations.Subject;
    import org.apache.kafka.clients.producer.RecordMetadata;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory; // Importar SLF4J Logger
    
    import java.util.Collections;
    import java.util.Map;
    import java.util.concurrent.Future;
    
    import static net.serenitybdd.screenplay.Tasks.instrumented;
    
    /**
     * Task para publicar mensajes en un topic de Kafka.
     * Sigue el patrón Screenplay donde las tareas describen QUÉ hace el actor,
     * delegando la lógica de conexión a una Habilidad.
     */
    public class PublishMessage implements Task {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(PublishMessage.class); // Inicializar SLF4J Logger
    
        private final String topic;
        private final String key;
        private final String message;
        private final Map<String, String> headers; // Nuevo campo para headers
    
        // Constructor principal que incluye headers
        public PublishMessage(String topic, String key, String message, Map<String, String> headers) {
            this.topic = topic;
            this.key = key;
            this.message = message;
            this.headers = headers != null ? headers : Collections.emptyMap(); // Asegurar que headers no sea null
        }
    
        /**
         * Método estático para crear la tarea de forma fluida, iniciando con el topic.
         */
        public static PublishMessage to(String topic) {
            return instrumented(PublishMessage.class, topic, null, null, Collections.emptyMap());
        }
    
        /**
         * Especifica la clave del mensaje.
         */
        public PublishMessage withKey(String key) {
            // Crear una nueva instancia con la clave actualizada, preservando los otros campos
            return instrumented(PublishMessage.class, this.topic, key, this.message, this.headers);
        }
    
        /**
         * Especifica el contenido del mensaje.
         */
        public PublishMessage withPayload(String payload) {
            // Crear una nueva instancia con el payload actualizado, preservando los otros campos
            return instrumented(PublishMessage.class, this.topic, this.key, payload, this.headers);
        }
    
        /**
         * Especifica los headers del mensaje (opcional).
         */
        public PublishMessage withHeaders(Map<String, String> headers) {
            // Crear una nueva instancia con los headers actualizados, preservando los otros campos
            return instrumented(PublishMessage.class, this.topic, this.key, this.message, headers);
        }
    
        /**
         * Ejecuta la tarea de publicar el mensaje utilizando la Habilidad del Actor.
         */
        @Override
        @Subject("publishes message to topic {0}")
        public <T extends Actor> void performAs(T actor) {
            // Obtener la habilidad de producir eventos Kafka del actor
            ProduceKafkaEvents kafkaProducerAbility = ProduceKafkaEvents.as(actor);
    
            try {
                // Enviar el mensaje y esperar confirmación
                Future<RecordMetadata> future = kafkaProducerAbility.send(topic, key, message, headers);
                RecordMetadata metadata = future.get(); // Espera síncrona para asegurar entrega
                LOGGER.info("Published -> topic:{} partition:{} offset:{} timestamp:{}",
                        metadata.topic(), metadata.partition(), metadata.offset(), metadata.timestamp());
    
            } catch (Exception e) {
                LOGGER.error("Failed to publish message to topic: {}", topic, e); // Loguear el error con SLF4J
                throw new RuntimeException("Failed to publish message to topic: " + topic, e);
            }
        }
    }

@Daniel36x Daniel36x closed this Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant