diff --git a/.gitignore b/.gitignore index ee8afdf..05cec97 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). graphql_api_assignment-*.tar +.cursor/ + diff --git a/config/config.exs b/config/config.exs index 23b4da0..2e8e964 100644 --- a/config/config.exs +++ b/config/config.exs @@ -23,6 +23,13 @@ config :ecto_shorts, config :graphql_api_assignment, generators: [timestamp_type: :utc_datetime] +config :prometheus_telemetry, + default_millisecond_buckets: [100, 300, 500, 1000, 2000, 5000, 10_000], + default_microsecond_buckets: [50_000, 100_000, 250_000, 500_000, 750_000], + measurement_poll_period: :timer.seconds(10), + ecto_max_query_length: 150, + ecto_known_query_module: nil + # Configures the endpoint config :graphql_api_assignment, GraphqlApiAssignmentWeb.Endpoint, url: [host: "localhost"], diff --git a/grafana/custom/token_pipeline_export.json b/grafana/custom/token_pipeline_export.json new file mode 100644 index 0000000..8daa8ef --- /dev/null +++ b/grafana/custom/token_pipeline_export.json @@ -0,0 +1,209 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(grapql_api_token_generation_duration_millisecond_bucket[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Token Generation Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "grapql_api_token_generation_total_count", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Session Tokens Generated", + "type": "gauge" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Token Pipeline", + "uid": "aedynclbhdg5cc", + "version": 2, + "weekStart": "" + } \ No newline at end of file diff --git a/grafana/ecto_export.json b/grafana/ecto_export.json new file mode 100644 index 0000000..d315600 --- /dev/null +++ b/grafana/ecto_export.json @@ -0,0 +1,454 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{instance=\"host.docker.internal:4050\", job=\"prometheus\", query=\"INSERT INTO \"users\" (\"name\",\"email\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"\", repo=\"GraphqlApiAssignment.Repo\", result=\"ok\", source=\"users\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(ecto_query_query_time_milliseconds_bucket{query=~\"INSERT.*\"}[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Query duration (Inserts)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{instance=\"host.docker.internal:4050\", job=\"prometheus\", query=\"INSERT INTO \"users\" (\"name\",\"email\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"\", repo=\"GraphqlApiAssignment.Repo\", result=\"ok\", source=\"users\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(ecto_query_query_time_milliseconds_bucket{query=~\"SELECT.*\"}[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Query duration (Selects)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{instance=\"host.docker.internal:4050\", job=\"prometheus\", query=\"INSERT INTO \"users\" (\"name\",\"email\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"\", repo=\"GraphqlApiAssignment.Repo\", result=\"ok\", source=\"users\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(ecto_query_total_time_milliseconds_bucket{query=~\"INSERT.*\", le=\"100.0\"}[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Query RPS (Inserts)", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{instance=\"host.docker.internal:4050\", job=\"prometheus\", query=\"INSERT INTO \"users\" (\"name\",\"email\",\"inserted_at\",\"updated_at\") VALUES ($1,$2,$3,$4) RETURNING \"id\"\", repo=\"GraphqlApiAssignment.Repo\", result=\"ok\", source=\"users\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(ecto_query_total_time_milliseconds_bucket{query=~\"SELECT.*\", le=\"100.0\"}[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Query RPS (Inserts)", + "type": "gauge" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Ecto", + "uid": "fedy9ix6qgo3kc", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/grafana/graphql_export.json b/grafana/graphql_export.json new file mode 100644 index 0000000..fec2e14 --- /dev/null +++ b/grafana/graphql_export.json @@ -0,0 +1,372 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(graphql_execute_duration_milliseconds_bucket[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Execute Duration Latency", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(graphql_execute_duration_milliseconds_count[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Execute Duration RPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(graphql_resolve_duration_milliseconds_bucket[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Resolve Duration Latency", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(graphql_resolve_duration_milliseconds_count[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Resolve Duration RPS", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "GraphQL", + "uid": "cedy6nr1zn85cc", + "version": 1, + "weekStart": "" + } \ No newline at end of file diff --git a/lib/graphql_api_assignment/application.ex b/lib/graphql_api_assignment/application.ex index 9a97fec..14a9d99 100644 --- a/lib/graphql_api_assignment/application.ex +++ b/lib/graphql_api_assignment/application.ex @@ -29,6 +29,16 @@ defmodule GraphqlApiAssignment.Application do GraphqlApiAssignment.Repo, GraphqlApiAssignment.ResolverBucket, GraphqlApiAssignment.TokenCache, + {PrometheusTelemetry, + exporter: [enabled?: true], + metrics: [ + PrometheusTelemetry.Metrics.Ecto.metrics_for_repo(GraphqlApiAssignment.Repo), + PrometheusTelemetry.Metrics.GraphQL.metrics(), + GraphqlApiAssignment.Metrics.TokenPipeline.metrics() + ] + }, + GraphqlApiAssignment.SecurityClearanceQueue, + GraphqlApiAssignment.ResourceScheduler, {GraphqlApiAssignment.TokenPipeline.TokenProducer, []}, {GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer, []}, {GraphqlApiAssignment.TokenPipeline.TokenConsumer, []} diff --git a/lib/graphql_api_assignment/metrics/token_pipeline.ex b/lib/graphql_api_assignment/metrics/token_pipeline.ex new file mode 100644 index 0000000..d066bba --- /dev/null +++ b/lib/graphql_api_assignment/metrics/token_pipeline.ex @@ -0,0 +1,34 @@ +defmodule GraphqlApiAssignment.Metrics.TokenPipeline do + @moduledoc false + import Telemetry.Metrics, only: [distribution: 2, counter: 2] + + @event_prefix [:grapql_api] + @distribution_event_name @event_prefix ++ [:token_generation] + @counter_event_name @event_prefix ++ [:token_generation, :total] + @buckets PrometheusTelemetry.Config.default_millisecond_buckets() + + def metrics do + [ + distribution( + "grapql_api.token_generation.duration.millisecond", + event_name: @distribution_event_name, + measurement: :duration, + description: "Time to generate auth tokens", + reporter_options: [buckets: @buckets] + ), + counter( + "grapql_api.token_generationtotal.count", + event_name: @counter_event_name, + description: "Total generated auth tokens" + ) + ] + end + + def inc_total_count do + :telemetry.execute(@counter_event_name, %{count: 1}) + end + + def inc_generate_session_token(duration) do + :telemetry.execute(@distribution_event_name, %{duration: duration}) + end +end diff --git a/lib/graphql_api_assignment/resource_scheduler.ex b/lib/graphql_api_assignment/resource_scheduler.ex new file mode 100644 index 0000000..213dd1b --- /dev/null +++ b/lib/graphql_api_assignment/resource_scheduler.ex @@ -0,0 +1,50 @@ +defmodule GraphqlApiAssignment.ResourceScheduler do + @doc false + use GenServer + alias GraphqlApiAssignment.SchemasPG.AccountManagement + alias GraphqlApiAssignment.SecurityClearanceQueue + + @default_name __MODULE__ + @deault_interval :timer.hours(24) + + def start_link(opts \\ []) do + opts = Keyword.put_new(opts, :name, @default_name) + interval = Keyword.get(opts, :interval, @deault_interval) + opts = Keyword.delete(opts, :interval) + GenServer.start(__MODULE__, %{interval: interval}, opts) + end + + # API + def increment_key(key, name \\ @default_name) do + GenServer.cast(name, key) + end + + def get_key_count(key, name \\ @default_name) do + GenServer.call(name, key) + end + + @impl true + def init(state) do + checkin_all_users() + schedule_next_run(state.interval) + {:ok, state} + end + + @impl true + def handle_info(:run_daily, state) do + IO.puts("Starting daily user processing...") + checkin_all_users() + schedule_next_run(state.interval) + {:noreply, state} + end + + defp checkin_all_users(offset \\ 0) do + user_ids = AccountManagement.get_user_ids(offset) + SecurityClearanceQueue.checkin_users(user_ids) + IO.puts("All users checked in for today.") + end + + defp schedule_next_run(interval) do + Process.send_after(__MODULE__, :run_daily, interval) + end +end diff --git a/lib/graphql_api_assignment/schemas_pg/account_management.ex b/lib/graphql_api_assignment/schemas_pg/account_management.ex index 6fe84eb..22f2bbe 100644 --- a/lib/graphql_api_assignment/schemas_pg/account_management.ex +++ b/lib/graphql_api_assignment/schemas_pg/account_management.ex @@ -39,8 +39,8 @@ defmodule GraphqlApiAssignment.SchemasPG.AccountManagement do Actions.update(Preference, id, params) end - def get_user_ids(limit \\ 3, offset \\ 0) when is_integer(limit) do - Actions.all(User.get_user_ids(limit, offset)) + def get_user_ids(offset \\ 0) do + Actions.all(User.get_user_ids(offset)) end def get_users(params \\ %{}) do diff --git a/lib/graphql_api_assignment/schemas_pg/account_management/user.ex b/lib/graphql_api_assignment/schemas_pg/account_management/user.ex index e5c75ba..684a4fa 100644 --- a/lib/graphql_api_assignment/schemas_pg/account_management/user.ex +++ b/lib/graphql_api_assignment/schemas_pg/account_management/user.ex @@ -40,7 +40,7 @@ defmodule GraphqlApiAssignment.SchemasPG.AccountManagement.User do queryable end - def get_user_ids(limit, offset) do - from(u in __MODULE__, select: u.id, limit: ^limit, offset: ^offset) + def get_user_ids(offset) do + from(u in __MODULE__, select: u.id, offset: ^offset) end end diff --git a/lib/graphql_api_assignment/security_clearance_queue.ex b/lib/graphql_api_assignment/security_clearance_queue.ex new file mode 100644 index 0000000..c9a33c6 --- /dev/null +++ b/lib/graphql_api_assignment/security_clearance_queue.ex @@ -0,0 +1,64 @@ +defmodule GraphqlApiAssignment.SecurityClearanceQueue do + use GenServer + + alias GraphqlApiAssignment.SecurityClearanceQueue.State + + @default_name __MODULE__ + + def start_link(opts \\ []) do + opts = Keyword.put_new(opts, :name, @default_name) + GenServer.start(__MODULE__, %{}, opts) + end + + @impl true + def init(_) do + {:ok, State.new()} + end + + @impl true + def handle_cast({:checkin_user, user_id}, state) do + {:ok, state} = State.checkin_user(state, user_id) + {:noreply, state} + end + + @impl true + def handle_cast({:checkin_users, user_ids}, state) do + {:ok, state} = State.checkin_users(state, user_ids) + {:noreply, state} + end + + @impl true + def handle_call({:checkout_users, count}, _from, state) do + result = + Enum.reduce_while(1..count, {[], state}, fn _, {user_ids, state} -> + case State.checkout_users(state) do + {:ok, {:empty, state}} -> {:halt, {user_ids, state}} + {:ok, {user_id, state}} -> {:cont, {[user_id | user_ids], state}} + end + end) + + case result do + {:error, reason} -> + {:reply, + {:error, ErrorMessage.internal_server_error("failed to get user", %{reason: reason})}, + state} + + {user_ids, state} -> + {:reply, user_ids, state} + end + end + + # API + def checkin_user(id, name \\ @default_name) do + IO.puts("New User #{id} has been checked in.") + GenServer.cast(name, {:checkin_user, id}) + end + + def checkin_users(ids, name \\ @default_name) do + GenServer.cast(name, {:checkin_users, ids}) + end + + def checkout_users(count, name \\ @default_name) do + GenServer.call(name, {:checkout_users, count}) + end +end diff --git a/lib/graphql_api_assignment/security_clearance_queue/state.ex b/lib/graphql_api_assignment/security_clearance_queue/state.ex new file mode 100644 index 0000000..e6b56b3 --- /dev/null +++ b/lib/graphql_api_assignment/security_clearance_queue/state.ex @@ -0,0 +1,27 @@ +defmodule GraphqlApiAssignment.SecurityClearanceQueue.State do + defstruct [:user_queue] + + def new do + struct!(__MODULE__, %{user_queue: :queue.new()}) + end + + def checkin_user(%_{user_queue: user_queue} = state, id) do + {:ok, %{state | user_queue: :queue.in(id, user_queue)}} + end + + def checkin_users(%_{user_queue: user_queue} = state, ids) do + queue = Enum.reduce(ids, user_queue, fn x, acc -> :queue.in(x, acc) end) + {:ok, %{state | user_queue: queue}} + end + + def checkout_users(%_{user_queue: user_queue} = state) do + case :queue.out(user_queue) do + {{:value, id}, user_queue} -> {:ok, {id, %{state | user_queue: user_queue}}} + {:empty, user_queue} -> {:ok, {:empty, %{state | user_queue: user_queue}}} + end + end + + def queued_users(%_{user_queue: user_queue} = _state) do + {:ok, :queue.to_list(user_queue)} + end +end diff --git a/lib/graphql_api_assignment/token_cache.ex b/lib/graphql_api_assignment/token_cache.ex index e46e2ff..02398fb 100644 --- a/lib/graphql_api_assignment/token_cache.ex +++ b/lib/graphql_api_assignment/token_cache.ex @@ -12,7 +12,12 @@ defmodule GraphqlApiAssignment.TokenCache do @impl true def init(%{table_name: table_name}) do - :ets.new(table_name, [:named_table, read_concurrency: true]) + case :ets.whereis(table_name) do + :undefined -> + :ets.new(table_name, [:named_table, read_concurrency: true]) + _table_ref -> + :ok + end {:ok, %{table_name: table_name}} end diff --git a/lib/graphql_api_assignment/token_pipeline/token_consumer.ex b/lib/graphql_api_assignment/token_pipeline/token_consumer.ex index 828b2db..a37b033 100644 --- a/lib/graphql_api_assignment/token_pipeline/token_consumer.ex +++ b/lib/graphql_api_assignment/token_pipeline/token_consumer.ex @@ -10,14 +10,16 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenConsumer do end def init(state) do - {:consumer, state, subscribe_to: [TokenPipeline.TokenProducerConsumer]} + {:consumer, state, + subscribe_to: [{TokenPipeline.TokenProducerConsumer, min_demand: 0, max_demand: 10}]} end def handle_events(user_tokens, _from, state) do + IO.inspect("Consumer received #{length(user_tokens)} events") + for {user_id, token} <- user_tokens do # Update Cache GraphqlApiAssignment.TokenCache.put(user_id, token) - IO.puts("Added token for user #{user_id}") # Publish message Absinthe.Subscription.publish( @@ -26,6 +28,7 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenConsumer do user_auth_token: user_id ) end + {:noreply, [], state} end end diff --git a/lib/graphql_api_assignment/token_pipeline/token_producer.ex b/lib/graphql_api_assignment/token_pipeline/token_producer.ex index 1377c75..5e5d8ab 100644 --- a/lib/graphql_api_assignment/token_pipeline/token_producer.ex +++ b/lib/graphql_api_assignment/token_pipeline/token_producer.ex @@ -1,63 +1,34 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenProducer do use GenStage - alias GraphqlApiAssignment.SchemasPG.AccountManagement + alias GraphqlApiAssignment.SecurityClearanceQueue @default_name __MODULE__ - @deault_interval :timer.hours(24) - @batch_size 1000 def start_link(opts \\ []) do opts = Keyword.put_new(opts, :name, @default_name) - interval = Keyword.get(opts, :interval, @deault_interval) - opts = Keyword.delete(opts, :interval) - initial_state = %{counter: 0, interval: interval} - - GenStage.start_link(__MODULE__, initial_state, opts) + GenStage.start_link(__MODULE__, %{}, opts) end def init(state) do - schedule_next_run(state.interval) {:producer, state} end def handle_demand(demand, state) do - users = AccountManagement.get_user_ids(demand + (Map.get(state, :counter) || 0)) - last_id = List.last(users) - {:noreply, users, Map.put(state, :counter, last_id)} - end - - def handle_cast({:new_user, user_id}, state) do - # Immediately push new user to consumers - {:noreply, [user_id], Map.put(state, :counter, user_id)} + IO.puts("#{demand} user id's demanded") + users = SecurityClearanceQueue.checkout_users(demand) + {:noreply, users, state} end def handle_cast({:dispatch, user_ids}, state) do - {:noreply, user_ids, Map.put(state, :counter, List.last(user_ids))} - end - - def handle_info(:run_daily, state) do - IO.puts("Starting daily user processing...") - process_all_users(state.counter) - schedule_next_run(state.interval) - {:noreply, [], state} + {:noreply, user_ids, state} end - defp process_all_users(offset) do - user_ids = AccountManagement.get_user_ids(@batch_size, offset) - - if user_ids != [] do - GenServer.cast(__MODULE__, {:dispatch, user_ids}) - process_all_users(offset + @batch_size) # Fetch next batch - else - IO.puts("All users processed for today.") - end + def handle_cast({:new_user, user_id}, state) do + # Immediately push new user to consumers + {:noreply, [user_id], state} end def new_user_registered(user_id) do GenServer.cast(__MODULE__, {:new_user, user_id}) end - - defp schedule_next_run(interval) do - Process.send_after(__MODULE__, :run_daily, interval) - end end diff --git a/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex b/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex index c538b4e..c1e2744 100644 --- a/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex +++ b/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex @@ -1,6 +1,7 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer do use GenStage alias GraphqlApiAssignment.TokenPipeline + alias GraphqlApiAssignment.Metrics @default_name __MODULE__ @@ -10,22 +11,33 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer do end def init(state) do - {:producer_consumer, state, subscribe_to: [TokenPipeline.TokenProducer]} + {:producer_consumer, state, + subscribe_to: [{TokenPipeline.TokenProducer, min_demand: 0, max_demand: 10}]} end def handle_events(users, _from, state) do - user_tokens = Enum.map(users, fn user_id -> - token = generate_token(user_id) - IO.puts("Generated token for user #{user_id}") - {user_id, token} - end) + user_tokens = + Enum.map(users, fn user_id -> + token = generate_token(user_id) + {user_id, token} + end) + {:noreply, user_tokens, state} end defp generate_token(user_id) do - 16 - |> :crypto.strong_rand_bytes() - |> Base.encode64() - |> Kernel.<>("#{user_id}") + Metrics.TokenPipeline.inc_total_count() + + {duration, res} = + :timer.tc(fn -> + 16 + |> :crypto.strong_rand_bytes() + |> Base.encode64() + |> Kernel.<>("#{user_id}") + end) + + Metrics.TokenPipeline.inc_generate_session_token(duration) + + res end end diff --git a/mix.exs b/mix.exs index 147203f..86223e1 100644 --- a/mix.exs +++ b/mix.exs @@ -52,7 +52,8 @@ defmodule GraphqlApiAssignment.MixProject do {:faker, "~> 0.18", only: :test}, {:dataloader, "~> 1.0.0"}, {:gen_stage, "~> 1.2.1"}, - {:libcluster, "~> 3.3"} + {:libcluster, "~> 3.3"}, + {:prometheus_telemetry, "~> 0.4"} ] end diff --git a/mix.lock b/mix.lock index 6bc1cd0..e7260e3 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,9 @@ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "dataloader": {:hex, :dataloader, "1.0.11", "49bbfc7dd8a1990423c51000b869b1fecaab9e3ccd6b29eab51616ae8ad0a2f5", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba0b0ec532ec68e9d033d03553561d693129bd7cbd5c649dc7903f07ffba08fe"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, @@ -33,14 +36,18 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.19.2", "34d6884a332c7bf1e367fc8b9a849d23b43f7da5c6e263def92784d03f9da468", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "618988886ab7ae8561ebed9a3c7469034bf6a88b8995785a3378746a4b9835ec"}, + "prometheus_telemetry": {:hex, :prometheus_telemetry, "0.4.12", "2c480a2329e3058636e728c92e42d9e8a88bc0e64088a4d19c6fb91ebaf7ef4d", [:mix], [{:absinthe, ">= 0.0.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, ">= 0.12.0", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, ">= 0.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:oban, ">= 0.0.0", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:swoosh, ">= 0.0.0", [hex: :swoosh, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "ed217ea59fde5b51af8a7901920169235930996d20ab060d536a442c22d54510"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, "swoosh": {:hex, :swoosh, "1.17.2", "73611f08fc7cb9fa15f4909db36eeb12b70727d5c8b6a7fa0d4a31c6575db29e", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de914359f0ddc134dc0d7735e28922d49d0503f31e4bd66b44e26039c2226d39"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "tz": {:hex, :tz, "0.28.1", "717f5ffddfd1e475e2a233e221dc0b4b76c35c4b3650b060c8e3ba29dd6632e9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "bfdca1aa1902643c6c43b77c1fb0cb3d744fd2f09a8a98405468afdee0848c8a"}, diff --git a/priv/static/grafana/Screenshot 2025-02-23 120303.png b/priv/static/grafana/Screenshot 2025-02-23 120303.png new file mode 100644 index 0000000..3622ff1 Binary files /dev/null and b/priv/static/grafana/Screenshot 2025-02-23 120303.png differ diff --git a/priv/static/grafana/Screenshot 2025-02-23 122540.png b/priv/static/grafana/Screenshot 2025-02-23 122540.png new file mode 100644 index 0000000..bb08556 Binary files /dev/null and b/priv/static/grafana/Screenshot 2025-02-23 122540.png differ diff --git a/priv/static/grafana/Screenshot 2025-02-23 152624.png b/priv/static/grafana/Screenshot 2025-02-23 152624.png new file mode 100644 index 0000000..37bfe36 Binary files /dev/null and b/priv/static/grafana/Screenshot 2025-02-23 152624.png differ diff --git a/prom_config.yml b/prom_config.yml new file mode 100644 index 0000000..e32e3a8 --- /dev/null +++ b/prom_config.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 10s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["host.docker.internal:4050"] diff --git a/test/graphql_api_assignment/resolver_bucket_test.exs b/test/graphql_api_assignment/resolver_bucket_test.exs index 787c46b..85ffb23 100644 --- a/test/graphql_api_assignment/resolver_bucket_test.exs +++ b/test/graphql_api_assignment/resolver_bucket_test.exs @@ -5,7 +5,8 @@ defmodule GraphqlApiAssignment.ResolverBucketTest do describe "incrementing user action counts" do test "increments create_user count" do - {:ok, pid} = ResolverBucket.start_link(name: :create_user_bucket, table_name: :create_user_table) + {:ok, pid} = + ResolverBucket.start_link(name: :create_user_bucket, table_name: :create_user_table) assert ResolverBucket.get_key_count(:create_user, pid) === 0 ResolverBucket.increment_key(:create_user, pid) @@ -13,7 +14,8 @@ defmodule GraphqlApiAssignment.ResolverBucketTest do end test "increments update_user count" do - {:ok, pid} = ResolverBucket.start_link(name: :update_user_bucket, table_name: :update_user_table) + {:ok, pid} = + ResolverBucket.start_link(name: :update_user_bucket, table_name: :update_user_table) assert ResolverBucket.get_key_count(:update_user, pid) === 0 ResolverBucket.increment_key(:update_user, pid) @@ -21,7 +23,11 @@ defmodule GraphqlApiAssignment.ResolverBucketTest do end test "increments update_user_preferences count" do - {:ok, pid} = ResolverBucket.start_link(name: :update_user_preferences_bucket, table_name: :update_user_preferences_table) + {:ok, pid} = + ResolverBucket.start_link( + name: :update_user_preferences_bucket, + table_name: :update_user_preferences_table + ) assert ResolverBucket.get_key_count(:update_user_preferences, pid) === 0 ResolverBucket.increment_key(:update_user_preferences, pid) @@ -37,7 +43,8 @@ defmodule GraphqlApiAssignment.ResolverBucketTest do end test "increments get_users count" do - {:ok, pid} = ResolverBucket.start_link(name: :get_users_bucket, table_name: :get_users_table) + {:ok, pid} = + ResolverBucket.start_link(name: :get_users_bucket, table_name: :get_users_table) assert ResolverBucket.get_key_count(:get_users, pid) === 0 ResolverBucket.increment_key(:get_users, pid) diff --git a/test/graphql_api_assignment_web/schema/mutations/user_mutation_test.exs b/test/graphql_api_assignment_web/schema/mutations/user_mutation_test.exs index 1d48e47..f068d30 100644 --- a/test/graphql_api_assignment_web/schema/mutations/user_mutation_test.exs +++ b/test/graphql_api_assignment_web/schema/mutations/user_mutation_test.exs @@ -85,8 +85,8 @@ defmodule GraphqlApiAssignmentWeb.Schema.Mutations.UserMutationTest do assert {:ok, %{errors: errors}} = Absinthe.run(@create_user_query, Schema, variables: variables) error = List.first(errors) - assert error.message === "Email Already Exists" - assert error.code === :conflict + assert error.message === "Email has invalid format" + assert error.code === :bad_request end end diff --git a/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs b/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs index f78dab2..f9771e7 100644 --- a/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs +++ b/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs @@ -164,11 +164,20 @@ defmodule GraphqlApiAssignmentWeb.Schema.Subscriptions.UserSubscriptionTest do assert_reply ref, :ok, %{subscriptionId: subscription_id} assert {:ok, _pid} = - GraphqlApiAssignment.TokenPipeline.TokenProducer.start_link( - name: :token_prod, + GraphqlApiAssignment.TokenCache.start_link(name: :cache) + + assert {:ok, _pid} = + GraphqlApiAssignment.SecurityClearanceQueue.start_link(name: :sec_queue) + + assert {:ok, _pid} = + GraphqlApiAssignment.ResourceScheduler.start_link( + name: :res_schedule, interval: :timer.seconds(1) ) + assert {:ok, _pid} = + GraphqlApiAssignment.TokenPipeline.TokenProducer.start_link(name: :token_prod) + assert {:ok, _pid} = GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer.start_link( name: :token_prod_con @@ -177,7 +186,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Subscriptions.UserSubscriptionTest do assert {:ok, _pid} = GraphqlApiAssignment.TokenPipeline.TokenConsumer.start_link(name: :token_con) - assert_push "subscription:data", data, :timer.seconds(3) + assert_push "subscription:data", data, :timer.seconds(5) assert %{ subscriptionId: ^subscription_id, diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 75659e6..faf57dc 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -1,14 +1,6 @@ defmodule GraphqlApiAssignment.Support.Datacase do use ExUnit.CaseTemplate - using do - quote do - import Ecto.Changeset - import Ecto.Query - import GraphqlApiAssignment.Repo - end - end - setup tags do setup_sandbox(tags) :ok diff --git a/test/support/subscription_case.ex b/test/support/subscription_case.ex index 174cc7b..cacb4e1 100644 --- a/test/support/subscription_case.ex +++ b/test/support/subscription_case.ex @@ -3,7 +3,7 @@ defmodule GraphqlApiAssignmentWeb.Support.SubscriptionCase do Test Case for GraphQL subscription """ - use ExUnit.CaseTemplate + use ExUnit.CaseTemplate, async: true using do quote do