Mastodon: ejecución de Collectors de OpenTelemetry en producción con un equipo pequeño

Por Juliano Costa (Datadog), Tristan Sloughter (community), Johanna Öjeling (Grafana Labs), Damien Mathieu (Elastic), Tim Campbell (Mastodon) | 18 de marzo de 2026

Esta implementación de referencia describe cómo Mastodon, una organización sin ánimo de lucro que opera a escala global con un equipo notablemente pequeño, ejecuta el OpenTelemetry Collector en producción.

Mastodon de un vistazo

Mastodon es una plataforma de redes sociales descentralizada, gratuita y de código abierto, operada por una organización sin ánimo de lucro.

La descentralización no es aquí un término de marketing, sino un principio arquitectónico central. Cualquiera puede ejecutar su propio servidor de Mastodon, y esos servidores operados de forma independiente interoperan mediante protocolos abiertos como parte de lo que se conoce como el fediverso: una red federada de plataformas sociales independientes que se comunican entre sí usando protocolos estandarizados como ActivityPub. Al igual que ocurre con el correo electrónico, los usuarios pueden comunicarse entre instancias independientemente de quién las opere.

Esta filosofía no solo condiciona las decisiones de producto de Mastodon, sino también su enfoque de la observabilidad.

Estructura organizativa

Toda la organización Mastodon está formada por unas 20 personas, y la infraestructura de observabilidad (incluido el OpenTelemetry Collector) la gestiona un único ingeniero.

A pesar del reducido tamaño del equipo, Mastodon opera dos grandes instancias de Mastodon en producción:

  • mastodon.social

    Se ejecuta en Kubernetes con autoescalado entre 9 y 15 nodos (16 núcleos, 64 GB de RAM cada uno). El frontend web escala entre 5 y 20 pods, mientras que los distintos pools de workers de Sidekiq escalan entre 10 y 40 pods. De media, mastodon.social tiene entre 70 y 80 pods en ejecución en un momento dado. Esta plataforma gestiona hasta 300 000 usuarios activos al día y aproximadamente 10 millones de solicitudes por minuto.

  • mastodon.online

    Se ejecuta en Kubernetes con autoescalado entre 3 y 6 nodos (8 núcleos, 32 GB de RAM cada uno). El frontend web escala entre 3 y 10 pods, y los pools de Sidekiq escalan entre 5 y 15 pods, lo que da un promedio total de 20 a 30 pods. Esta instancia opera a una escala menor, pero aun así considerable.

Con un ancho de banda operativo tan limitado, la simplicidad y la fiabilidad no son negociables.

Adopción de OpenTelemetry: la libertad de elección por diseño

Dado que Mastodon es de código abierto y está diseñado para que otros lo ejecuten, el equipo quería una solución de telemetría que preservara la libertad del operador.

OpenTelemetry se convirtió en la opción predeterminada porque permite que cada operador de un servidor Mastodon decida cómo —o si— se recopila la telemetría.

Mediante una sencilla configuración por variables de entorno, los operadores pueden elegir:

  • Enviar la telemetría directamente a un backend de observabilidad (usando únicamente la configuración del SDK de Ruby)
  • Enrutar la telemetría a través de un OpenTelemetry Collector
  • Deshabilitar la telemetría por completo

La organización central de Mastodon no realiza seguimiento de cómo gestionan la observabilidad las instancias externas. Lo que importa es que la telemetría emitida se ajuste estrictamente a las convenciones semánticas de OpenTelemetry, lo que la hace utilizable en cualquier lugar.

Este enfoque evita los modelos de datos específicos de proveedor y garantiza la compatibilidad con el ecosistema más amplio de OpenTelemetry, sin que Mastodon tenga que mantener sus propias convenciones.

Arquitectura del Collector: uno solo por namespace

La arquitectura de Collector de Mastodon es intencionadamente minimalista.

Un único OpenTelemetry Collector por namespace de Kubernetes gestiona todas las señales de telemetría: trazas, métricas y logs. No hay niveles separados de gateway y agente, ni capas de enrutamiento complejas, ni herramientas de despliegue personalizadas.

Diagrama de arquitectura de los nodos de Mastodon

Dada la escala y el tráfico, esto ha demostrado ser más que suficiente.

Tim Campbell, ingeniero de software en Mastodon, comentó que en los aproximadamente 2 años que llevan ejecutando el Collector, nunca han tenido un solo problema con él.

«Para mi sorpresa, mi muy grata sorpresa, no he tenido ni un solo problema. Como usamos un operador de Kubernetes para esto, si alguna vez surge algún problema, simplemente se reinicia automáticamente. Al menos en lo que respecta a las trazas y los logs reales que llegan a Datadog, no he visto ninguna interrupción. En cuanto a memoria y procesos, se ha mantenido perfectamente estable dentro de los límites que hemos establecido.»

Despliegue y gestión del ciclo de vida

Para mantener la sobrecarga operativa lo más baja posible, Mastodon se apoya en:

Cada Collector se define como un recurso personalizado OpenTelemetryCollector. A partir de ahí, Kubernetes se encarga automáticamente de la reconciliación, los reinicios y la gestión del ciclo de vida.

«Básicamente, solo necesitamos crear un archivo yaml para cada objeto OpenTelemetryCollector que necesitemos crear, y Argo se encarga de desplegar/actualizar automáticamente lo que necesitamos.»

Este modelo proporciona:

  • Configuración declarativa
  • Recuperación automática ante fallos
  • Auditabilidad clara a través del historial de Git

Cabe destacar que Mastodon no impone límites estrictos de CPU o memoria a los pods del Collector. En la práctica, el consumo de recursos se ha mantenido insignificante en comparación con el resto de la plataforma.

Gestión del tráfico mediante el muestreo

En lugar de basarse en límites de recursos, Mastodon controla la sobrecarga de observabilidad principalmente mediante el muestreo basado en cola (tail-based sampling).

  • En mastodon.social, las trazas exitosas se muestrean aproximadamente al 0,1 %, lo que da como resultado solo unas pocas docenas de trazas por minuto a pesar del tráfico extremadamente alto.
  • En mastodon.online, el muestreo es ligeramente más permisivo, pero sigue los mismos principios.
  • Todas las trazas de error se recopilan siempre, lo que garantiza una visibilidad completa de los fallos.

Este enfoque mantiene el volumen de datos predecible al tiempo que conserva los datos de diagnóstico de alto valor.

Configuración: con criterio propio, pero mínima

Mastodon usa la distribución OpenTelemetry Collector Contrib, principalmente por conveniencia: incluye todo lo que necesitan sin requerir compilaciones personalizadas.

La configuración se centra en:

  • Ingesta OTLP para todas las señales
  • Enriquecimiento de metadatos de Kubernetes
  • Detección de recursos
  • Muestreo basado en la cola (tail-based sampling)
  • Transformación para la compatibilidad con el backend

A continuación se incluye una configuración completa de producción, a modo de referencia (también puedes verla en otelbin):

Configuración del Collector de Mastodon
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: mastodon-social
  namespace: mastodon-social
spec:
  nodeSelector:
    joinmastodon.org/property: mastodon.social
  env:
    - name: DD_API_KEY
      valueFrom:
        secretKeyRef:
          name: datadog-secret
          key: api-key
    - name: DD_SITE
      valueFrom:
        secretKeyRef:
          name: datadog-secret
          key: site
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
            cors:
              allowed_origins:
                - 'http://*'
                - 'https://*'

    processors:
      batch: {}
      resource:
        attributes:
          - key: deployment.environment.name
            value: 'production'
            action: upsert
          - key: property
            value: 'mastodon.social'
            action: upsert
          - key: git.commit.sha
            from_attribute: vcs.repository.ref.revision
            action: insert
          - key: git.repository_url
            from_attribute: vcs.repository.url.full
            action: insert
      k8sattributes:
        auth_type: 'serviceAccount'
        passthrough: false
        extract:
          metadata:
            - k8s.namespace.name
            - k8s.pod.name
            - k8s.pod.start_time
            - k8s.pod.uid
            - k8s.deployment.name
            - k8s.node.name
          labels:
            - tag_name: app.label.component
              key: app.kubernetes.io/component
              from: pod
        pod_association:
          - sources:
              - from: resource_attribute
                name: k8s.pod.ip
          - sources:
              - from: resource_attribute
                name: k8s.pod.uid
          - sources:
              - from: connection
      resourcedetection:
        detectors: [system]
        system:
          resource_attributes:
            os.description:
              enabled: true
            host.arch:
              enabled: true
            host.cpu.vendor.id:
              enabled: true
            host.cpu.family:
              enabled: true
            host.cpu.model.id:
              enabled: true
            host.cpu.model.name:
              enabled: true
            host.cpu.stepping:
              enabled: true
            host.cpu.cache.l2.size:
              enabled: true
      transform:
        error_mode: ignore

        # Nomenclatura correcta de la función de código
        trace_statements:
          - context: span
            conditions:
              - attributes["code.namespace"] != nil
            statements:
              - set(attributes["resource.name"],
                Concat([attributes["code.namespace"],
                attributes["code.function"]], "#"))

          # Nombre de host de Kubernetes correcto
          - context: resource
            conditions:
              - attributes["k8s.node.name"] != nil
            statements:
              - set (attributes["k8s.node.name"],
                Concat([attributes["k8s.node.name"], "k8s-1"], "-"))
        metric_statements:
          - context: resource
            conditions:
              - attributes["k8s.node.name"] != nil
            statements:
              - set (attributes["k8s.node.name"],
                Concat([attributes["k8s.node.name"], "k8s-1"], "-"))
        log_statements:
          - context: resource
            conditions:
              - attributes["k8s.node.name"] != nil
            statements:
              - set (attributes["k8s.node.name"],
                Concat([attributes["k8s.node.name"], "k8s-1"], "-"))
      attributes/sidekiq:
        include:
          match_type: strict
          attributes:
            - key: messaging.sidekiq.job_class
        actions:
          - key: resource.name
            from_attribute: messaging.sidekiq.job_class
            action: upsert
      tail_sampling:
        policies:
          [
            {
              name: errors-policy,
              type: status_code,
              status_code: { status_codes: [ERROR] },
            },
            {
              name: randomized-policy,
              type: probabilistic,
              probabilistic: { sampling_percentage: 0.1 },
            },
          ]

    connectors:
      datadog/connector:
        traces:
          compute_stats_by_span_kind: true

    exporters:
      datadog:
        api:
          site: ${DD_SITE}
          key: ${DD_API_KEY}
        traces:
          compute_stats_by_span_kind: true
          trace_buffer: 500

    service:
      pipelines:
        traces/all:
          receivers: [otlp]
          processors:
            [
              resource,
              k8sattributes,
              resourcedetection,
              transform,
              attributes/sidekiq,
              batch,
            ]
          exporters: [datadog/connector]
        traces/sample:
          receivers: [datadog/connector]
          processors: [tail_sampling, batch]
          exporters: [datadog]
        metrics:
          receivers: [datadog/connector, otlp]
          processors:
            [resource, k8sattributes, resourcedetection, transform, batch]
          exporters: [datadog]
        logs:
          receivers: [otlp]
          processors:
            [
              resource,
              k8sattributes,
              resourcedetection,
              transform,
              attributes/sidekiq,
              batch,
            ]
          exporters: [datadog]

Mantenerse actualizado

Mastodon normalmente actualiza el OpenTelemetry Collector en el plazo de uno o dos días tras cada versión.

«Todo está documentado, y todos los cambios incompatibles están correctamente detallados», señaló Tim, elogiando la claridad de las notas de la versión.

Aunque las versiones frecuentes a veces introducen cambios incompatibles, el equipo lo considera una señal de un desarrollo saludable y activo, siempre que te mantengas al día.

Lecciones y puntos de dolor

La parte más difícil del recorrido fue, simplemente, empezar. Entender cómo encajan entre sí los componentes del Collector llevó tiempo, especialmente para un equipo sin especialistas dedicados a la observabilidad. Más recientemente, la mayor complejidad ha surgido del uso avanzado del procesador transform, en particular al adaptar los atributos de span a los requisitos de nomenclatura específicos del backend.

transform:
  error_mode: ignore

  # Nomenclatura correcta de la función de código
  trace_statements:
    - context: span
      conditions:
        - attributes["code.namespace"] != nil
      statements:
        - set(attributes["resource.name"], Concat([attributes["code.namespace"],
          attributes["code.function"]], "#"))

En la regla del procesador transform anterior, han configurado una condición para establecer resource.name (un atributo específico de Datadog) con el valor de code.namespace#code.function. Con esa configuración, cada vez que el span llegaba al backend, podía asignarse al nombre que habían definido. A pesar de esa curva de aprendizaje, la experiencia general ha superado las expectativas.

«Básicamente puedes hacer lo que quieras. Superó mis expectativas. Todo funciona bastante bien.»

Esa fiabilidad y flexibilidad son las razones por las que Mastodon sigue usando el OpenTelemetry Collector en producción.

Consejos para equipos pequeños

A partir de la experiencia de Mastodon, destacan algunas lecciones:

  • Mantén la arquitectura simple: un único Collector puede llegar muy lejos
  • Confía en los operadores de Kubernetes para la gestión del ciclo de vida
  • Usa el muestreo para controlar los costes
  • Cíñete a las convenciones semánticas para evitar la dependencia de proveedor (lock-in) a largo plazo
  • Actualiza con frecuencia para reducir el impacto de los cambios incompatibles

Conclusiones

La historia de Mastodon demuestra que incluso un equipo muy pequeño puede operar con éxito OpenTelemetry Collectors en producción —a escala global— sin una carga operativa significativa.