{# The final Go code is autogenerated, but this template is not. Please file bugs! #}
package glean

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT.

// required imports
import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "strconv"
    "time"

    "github.com/google/uuid"
)

// log type string used to identify logs to process in the Moz Data Pipeline
var gleanEventMozlogType string = "glean-server-event"

// A GleanEventsLogger produces output in the required format for Glean to ingest.
// Glean ingestion requires the output to be written to os.Stdout. Writing to a different
// output will require the consumer to handle any closing as appropriate for the Writer.
// e.g. if writing to a file.
type GleanEventsLogger struct {
    AppID             string // Application Id to identify application per Glean standards
    AppDisplayVersion string // Version of application emitting the event
    AppChannel        string // Channel to differentiate logs from prod/beta/staging/devel
    Writer            io.Writer // Writer to output to. Normal operation expects os.Stdout
}

// exported type for public method parameters
type RequestInfo struct {
    UserAgent string
    IpAddress string
}

// default empty values will be omitted in json from ping struct definition
var defaultRequestInfo = RequestInfo{
    UserAgent: "",
    IpAddress: "",
}

// structs to construct the glean ping
type clientInfo struct {
    TelemetrySDKBuild string `json:"telemetry_sdk_build"`
    FirstRunDate      string `json:"first_run_date"`
    OS                string `json:"os"`
    OSVersion         string `json:"os_version"`
    Architecture      string `json:"architecture"`
    AppBuild          string `json:"app_build"`
    AppDisplayVersion string `json:"app_display_version"`
    AppChannel        string `json:"app_channel"`
}

type pingInfo struct {
    Seq       int    `json:"seq"`
    StartTime string `json:"start_time"`
    EndTime   string `json:"end_time"`
}

type ping struct {
    DocumentNamespace string `json:"document_namespace"`
    DocumentType      string `json:"document_type"`
    DocumentVersion   string `json:"document_version"`
    DocumentID        string `json:"document_id"`
    UserAgent         string `json:"user_agent,omitempty"`
    IpAddress         string `json:"ip_address,omitempty"`
    Payload           string `json:"payload"`
}

type metrics map[string]map[string]interface{}

type pingPayload struct {
    ClientInfo clientInfo   `json:"client_info"`
    PingInfo   pingInfo     `json:"ping_info"`
    Metrics    metrics      `json:"metrics"`
    Events     []gleanEvent `json:"events"`
}

type gleanEvent struct {
    Category  string            `json:"category"`
    Name      string            `json:"name"`
    Timestamp int64             `json:"timestamp"`
    Extra     map[string]string `json:"extra"`
}

type logEnvelope struct {
    Timestamp string
    Logger    string
    Type      string
    Fields    ping
}

func (g GleanEventsLogger) createClientInfo() clientInfo {
    // Fields with default values are required in the Glean schema, but not used in server context
    return clientInfo{
        TelemetrySDKBuild: "glean_parser v{{ parser_version }}",
        FirstRunDate:      "Unknown",
        OS:                "Unknown",
        OSVersion:         "Unknown",
        Architecture:      "Unknown",
        AppBuild:          "Unknown",
        AppDisplayVersion: g.AppDisplayVersion,
        AppChannel:        g.AppChannel,
    }
}

func createPingInfo() pingInfo {
    {# times are ISO-8601 strings, e.g. "2023-12-19T22:09:17.440Z" #}
    now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
    return pingInfo{
        Seq:       0,
        StartTime: now,
        EndTime:   now,
    }
}

func (g GleanEventsLogger) createPing(documentType string, config RequestInfo, payload pingPayload) (ping, error) {
    payloadJson, err := json.Marshal(payload)
    if err != nil {
        return ping{}, err
    }

    documentID, err := uuid.NewRandom()
    if err != nil {
        return ping{}, err
    }

    return ping{
        DocumentNamespace: g.AppID,
        DocumentType:      documentType,
        DocumentVersion:   "1",
        DocumentID:        documentID.String(),
        UserAgent:         config.UserAgent,
        IpAddress:         config.IpAddress,
        Payload:           string(payloadJson),
    }, nil
}

// method called by each ping-specific record method.
// construct the ping, wrap it in the envelope, and print to stdout
func (g GleanEventsLogger) record(
    documentType string,
    requestInfo RequestInfo,
    metrics metrics,
    events []gleanEvent,
) error {
    if g.Writer == nil {
        return errors.New("writer not specified")
    }

    telemetryPayload := pingPayload{
        ClientInfo: g.createClientInfo(),
        PingInfo:   createPingInfo(),
        Metrics:    metrics,
        Events:     events,
    }

    ping, err := g.createPing(documentType, requestInfo, telemetryPayload)
    if err != nil {
        return err
    }

    envelope := logEnvelope{
        Timestamp: strconv.FormatInt(time.Now().UnixNano(), 10),
        Logger:    "glean",
        Type:      gleanEventMozlogType,
        Fields:    ping,
    }
    envelopeJson, err := json.Marshal(envelope)
    if err != nil {
        return err
    }

    fmt.Fprintln(g.Writer, string(envelopeJson))
    return nil
}
{# if any ping has an event metric, create methods and types for them #}
{% if events %}

func newGleanEvent(category, name string, extra map[string]string) gleanEvent {
    return gleanEvent{
        Category:  category,
        Name:      name,
        Timestamp: time.Now().UnixMilli(),
        Extra: extra,
    }
}
{# each event has a type and method to create a gleanEvent #}
{% for event in events %}

type {{ event|event_type_name }} struct {
    {% for extra, metadata in event.extra_keys.items() %}
    {{ extra|event_extra_name }} {{ metadata.type|go_metric_type }} // {{ metadata.description|clean_string }}
    {% endfor %}
}

func (e {{ event|event_type_name }}) gleanEvent() gleanEvent {
    return newGleanEvent(
        "{{ event.category }}",
        "{{ event.name }}",
        map[string]string{
        {% for extra, metadata in event.extra_keys.items() %}
            {# convert all extra fields to string for submission #}
            {% if metadata.type == 'boolean' %}
            "{{ extra }}": fmt.Sprintf("%t", e.{{ extra|event_extra_name }}),
            {% elif metadata.type == 'quantity' %}
            "{{ extra }}": fmt.Sprintf("%d", e.{{ extra|event_extra_name }}),
            {% else %}
            "{{ extra }}": e.{{ extra|event_extra_name }},
            {% endif %}
        {% endfor %}
        },
    )
}
{% endfor %}
{% endif %}
{# struct & methods for submitting pings #}
{% for ping, metrics_by_type in pings.items() %}
{% if metrics_by_type['event'] %}

{# interface that eligible events for this ping will implement #}
type {{ ping|ping_events_type_name }} interface {
    is{{ping|ping_events_type_name }}()
    gleanEvent() gleanEvent
}

{# create functions for valid events for this ping #}
{% for metric in metrics_by_type['event'] %}
func (e {{ metric|event_type_name}}) is{{ ping|ping_events_type_name }}() {}
{% endfor %}
{% endif %}

type {{ ping|ping_type_name }} struct {
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
    {% for metric in metrics %}
    {{ metric|metric_argument_name }} {{ metric_type|go_metric_type}} // {{ metric.description|clean_string }}
    {% endfor %}
    {% endif %}
    {% endfor %}
    {% if metrics_by_type['event'] %}
    Event {{ ping|ping_events_type_name }} // valid event for this ping
    {% endif %}
}

// Record and submit `{{ ping }}` ping
func (g GleanEventsLogger) Record{{ ping|ping_type_name }}(
    requestInfo RequestInfo,
    params {{ ping|ping_type_name }},
) error {
    metrics := metrics{
    {% for metric_type, metrics in metrics_by_type.items() %}
    {% if metric_type != 'event' %}
        "{{ metric_type }}": {
        {% for metric in metrics %}
            {% if metric_type == 'datetime' %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }}.Format("2006-01-02T15:04:05.000Z"),
            {% else %}
            "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }},
            {% endif %}
        {% endfor %}
        },
    {% endif %}
    {% endfor %}
    }

    events := []gleanEvent{}
    {% if metrics_by_type['event'] %}
    if params.Event != nil {
        events = append(events, params.Event.gleanEvent())
    }
    {% endif %}
    return g.record("{{ ping }}", requestInfo, metrics, events)
}

// Record and submit `{{ ping }}` ping omitting user request info
func (g GleanEventsLogger) Record{{ ping|ping_type_name}}WithoutUserInfo(
    params {{ ping|ping_type_name}},
) error {
    return g.Record{{ ping|ping_type_name }}(defaultRequestInfo, params)
}
{% endfor %}
