dart-lang / shelf

Web server middleware for Dart
https://pub.dev/packages/shelf
BSD 3-Clause "New" or "Revised" License
921 stars 124 forks source link

add a method to group routes and apply specific middleware to this group #393

Open insinfo opened 10 months ago

insinfo commented 10 months ago

add a method to group routes and apply specific middleware to this group

  app.chain([simulatedMiddleware]).group('/api/v1', (router) {
      for (final sr in simulatedRoutes) {
        final method = sr.keys.first.toUpperCase();
        final path = sr.values.first;
        router.addRoute(method, path, (req, res) => simulatedWork);
      }
    });
insinfo commented 10 months ago

maybe something like that

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:prometheus_client/format.dart' as format;
import 'package:prometheus_client/prometheus_client.dart';
import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_router/src/router_entry.dart';
import 'package:stack_trace/stack_trace.dart';

import 'package:dart_segment_fault/dependencies/shelf_cors_headers_base/shelf_cors_headers_base.dart';
import 'package:dart_segment_fault/dependencies/stream_isolate/stream_isolate.dart';

// to test
// xargs -I % -P 8 curl "http:/192.168.66.123:3161/api/v1/protocolo/processos/public/site/2023/10" < <(printf '%s\n' {1..400})
//./wrk -t12 -c400 -d30s http://172.30.82.2:3350

const defaultHeaders = {'Content-Type': 'application/json;charset=utf-8'};
final streamIsolates = <Map<int, BidirectionalStreamIsolate>>[];
void main(List<String> args) async {
  final parser = ArgParser()
    ..addOption('address', abbr: 'a', defaultsTo: '0.0.0.0')
    ..addOption('port', abbr: 'p', defaultsTo: '3161')
    ..addOption('isolates', abbr: 'j', defaultsTo: '3');

  final argsParsed = parser.parse(args);
  final arguments = [argsParsed['address'], int.parse(argsParsed['port'])];

  final numberOfIsolates = int.parse(argsParsed['isolates']);
  for (var i = 0; i < numberOfIsolates - 1; i++) {
    final streamIsolate = await StreamIsolate.spawnBidirectional(isolateMain,
        debugName: i.toString(), argument: [i, ...arguments]);
    streamIsolates.add({i: streamIsolate});
    streamIsolate.stream.listen((event) => receiveAndPass(event, i));
  }
}

/// receive msg from isolate and send to all isolates
void receiveAndPass(event, int idx) {
  streamIsolates.forEach((item) {
    item.values.first.send(event);
  });
}

Stream isolateMain(Stream inc, dynamic args) {
  final arguments = args as List;
  int id = arguments[0];
  String address = arguments[1];
  int port = arguments[2];

  final streamController = StreamController.broadcast();

  final reg = CollectorRegistry(); //CollectorRegistry.defaultRegistry;
  // Register default runtime metrics
  runtime_metrics.register(reg);
  // Register http requests total
  final http_requests_total = Counter(
      name: 'http_requests_total', help: 'Total number of http api requests');
  http_requests_total.register(reg);
  // listen msg from main
  inc.listen((msg) {
    http_requests_total.inc();
  });

  _startServer([id, streamController, reg, address, port]);
  return streamController.stream;
}

void _startServer(List args) async {
  final streamController = args[1] as StreamController;
  final reg = args[2] as CollectorRegistry;
  String address = args[3];
  int port = args[4];

  final app = Router();
  routes(app, reg);

  final handler =
      Pipeline().addMiddleware(corsHeaders()).addMiddleware((innerHandler) {
    return (request) async {
      // Every time http_request is called, increase the counter by one
      final resp = await innerHandler(request);
      if (!request.url.path.contains('metrics')) {
        //send msg to main
        streamController.add('+1');
      }
      return resp;
    };
  })
          //.addMiddleware(logRequestsCustom())
          .addHandler(app);

  final server = await io.serve(handler, address, port, shared: true);
  server.defaultResponseHeaders.remove('X-Frame-Options', 'SAMEORIGIN');
  print('Serving at http://${server.address.host}:${server.port}');
}

void routes(Router app, CollectorRegistry reg) {
  app.get('/', (Request request) async {
    return Response.ok('shelf');
  });

  // Register a handler to expose the metrics in the Prometheus text format
  app.get('/metrics', (Request request) async {
    final buffer = StringBuffer();
    final metrics = await reg.collectMetricFamilySamples();
    format.write004(buffer, metrics);
    return Response.ok(
      buffer.toString(),
      headers: {'Content-Type': format.contentType},
    );
  });
  app.group('/api/v1', (router) {
    for (final sr in simulatedRoutes) {
      final method = sr.keys.first.toUpperCase();
      final path = sr.values.first;
      router.add(method, path, simulatedWork);
    }
  },middleware: simulatedMiddleware);
}

class RouterContext {
  final Router _router;
  final String basePath;
  final Middleware? middleware;
  RouterContext(
    this._router, {
    required this.basePath,
    this.middleware,
  });
  void add(
      String verb, String route, FutureOr<Response> Function(Request) handler) {
    if (middleware != null) {
      _router.add(verb, basePath + route, middleware!(handler));
    } else {
      _router.add(verb, basePath + route, handler);
    }
  }
}

extension RouterExtension on Router {
  void group(String basePath, Function(RouterContext) callback,
      {Middleware? middleware}) {
    final ctx = RouterContext(this, basePath: basePath);
    callback(ctx);
  }
}

FutureOr<Response> Function(Request) simulatedMiddleware(innerHandler) {
  return (request) async {
    final resp = await innerHandler(request);
    return resp;
  };
}

Future<Response> simulatedWork(Request request) async {
  try {
    final listItems = List.generate(150, (int index) => {'name': 'Jon_$index'});
    return Response.ok(
      jsonEncode(listItems),
      headers: defaultHeaders,
    );
  } catch (e, s) {
    print('simulatedWork@all $e $s');
    return Response.badRequest();
  }
}

final simulatedRoutes = [
  {'get': '/administracao/paises'},
  {'get': '/administracao/ufs'},
  {'get': '/administracao/municipios'},
  {'get': '/administracao/modulos'},
  {'get': '/administracao/permissoes/:numCgm/:anoExercicio'},
  {'put': '/administracao/permissoes/:numCgm/:anoExercicio'},
  {'get': '/administracao/escolaridades'},
  {'get': '/administracao/tiposlogradouro'},
  {'get': '/administracao/orgaos'},
  {'get': '/administracao/unidades'},
  {'get': '/administracao/departamentos'},
  {'get': '/administracao/setores'},
  {'get': '/administracao/gestao'},
  {'get': '/administracao/usuarios'},
  {'get': '/administracao/usuarios/:numcgm'},
  {'post': '/administracao/usuarios'},
  {'put': '/administracao/usuarios'},
  {'get': '/administracao/cgm'},
  {'get': '/administracao/cgm/:cgm'},
  {'get': '/administracao/auditorias'},
  {'post': '/administracao/auditorias'},
  {'get': '/administracao/configuracao'},
  {'get': '/administracao/configuracao/by/filtro'},
  {'get': '/administracao/funcionalidades'},
  {'get': '/administracao/menu/:cgm'},
  {'get': '/administracao/organograma/hierarquia'},
  {'get': '/administracao/acoes'},
  //auth
  {'post': '/change/pass'},
  {'get': '/auth/check/permissao/:cgm'},
  {'post': '/auth/login'},
  {'post': '/auth/check'},
  {'get': '/auth/check/toke'},
  //cgm
  {'get': '/cgm/full'},
  {'get': '/cgm/full/:cgm'},
  {'delete': '/cgm'},
  {'post': '/cgm/full'},
  {'post': '/cgm/full/interno'},
  {'put': '/cgm/full'},
  {'get': '/cgm/atributos'},
  {'get': '/cgm/atributos/:cgm'},
  {'get': '/cgm/categoriashabilitacao'},
  {'get': '/cgm/tiposlogradouro'},
  //
  {'get': '/estatistica/processos/ano'},
  {'get': '/estatistica/processos/periodo/setor/primero/tramite'},
  {'get': '/estatistica/processos/situacao'},
  {'get': '/estatistica/processos/classificacao'},
  {'get': '/estatistica/processos/assunto'},
  //
  {'get': '/norma/normas'},
  //
  {'get': '/protocolo/processos/favoritos/cgm/:cgm'},
  {'post': '/protocolo/processos/favoritos'},
  {'post': '/protocolo/processos/favoritos/:codProcesso/:anoExercicio'},
  {'put': '/protocolo/processos/favoritos'},
  {'delete': '/protocolo/processos/favoritos/:id'},
  {'delete': '/protocolo/processos/favoritos/:codProcesso/:anoExercicio'},
  {'get': '/protocolo/acoes/favoritas/cgm/:cgm'},
  {'post': '/protocolo/acoes/favoritas'},
  {'post': '/protocolo/acoes/favoritas/:codAcao'},
  {'put': '/protocolo/acoes/favoritas'},
  {'delete': '/protocolo/acoes/favoritas/:id'},
  {'delete': '/protocolo/acoes/favoritas/:codAcao'},
  {'get': '/protocolo/assuntos'},
  {'post': '/protocolo/assuntos'},
  {'put': '/protocolo/assuntos'},
  {'delete': '/protocolo/assuntos'},
  {'get': '/protocolo/assuntos/:codAssunto/:codClassificacao'},
  {'get': '/protocolo/classificacoes'},
  {'post': '/protocolo/classificacoes'},
  {'put': '/protocolo/classificacoes'},
  {'delete': '/protocolo/classificacoes'},
  {'get': '/protocolo/despachospadrao'},
  {'post': '/protocolo/despachospadrao'},
  {'put': '/protocolo/despachospadrao'},
  {'delete': '/protocolo/despachospadrao'},
  {'get': '/protocolo/tramites'},
  {'post': '/protocolo/tramites'},
  {
    'put':
        '/protocolo/tramites/:codClassiOld/:codAssuntoOld/:ordemOld/:exercicioOld'
  },
  {'delete': '/protocolo/tramites'},
  {'get': '/protocolo/processos/:anoExercicio/:codProcesso'},
  {'get': '/protocolo/processos/em/apenso/:anoExercicio/:codProcesso'},
  {'get': '/protocolo/processos/apenso/a/:anoExercicio/:codProcesso'},
  {'get': '/protocolo/processos/andamentos/:anoExercicio/:codProcesso'},
  {
    'get':
        '/protocolo/processos/despachos/:anoExercicio/:codProcesso/:codAndamento/:codUsuario/:timestamp'
  },
  {'get': '/protocolo/processos'},
  {'get': '/protocolo/processos/areceber'},
  {'get': '/protocolo/processos/aemcaminhar'},
  {'get': '/protocolo/processos/byfiltros'},
  {'get': '/protocolo/processos/aapensara'},
  {'get': '/protocolo/processos/adesapensar'},
  {'get': '/protocolo/processos/adespachar'},
  {'get': '/protocolo/processos/aalterar'},
  {'get': '/protocolo/processos/acancelar'},
  {'get': '/protocolo/processos/aarquivar'},
  {'get': '/protocolo/processos/adesarquivar'},
  {'post': '/protocolo/processos'},
  {'post': '/protocolo/processos/implantar'},
  {'put': '/protocolo/processos'},
  {'post': '/protocolo/processos/receber/lote'},
  {'post': '/protocolo/processos/despachar/lote'},
  {'post': '/protocolo/processos/encaminhar/lote'},
  {'post': '/protocolo/processos/cancelarencaminhamento/lote'},
  {'post': '/protocolo/processos/apensar/lote'},
  {'post': '/protocolo/processos/desapensar/lote'},
  {'post': '/protocolo/processos/arquivar/lote'},
  {'post': '/protocolo/processos/desarquivar/lote'},
  {'post': '/protocolo/processos/anexos'},
  {'get': '/protocolo/processos/anexos/:codProcesso/:anoExercicio'},
  {'get': '/protocolo/situacoes'},
  {'get': '/protocolo/historicoarquivamento'},
  {'post': '/protocolo/historicoarquivamento'},
  {'put': '/protocolo/historicoarquivamento'},
  {'delete': '/protocolo/historicoarquivamento'},
  {'get': '/protocolo/listagemprocessos'},
  {'post': '/protocolo/listagemprocessos'},
  {'get': '/protocolo/documentos'},
  {'post': '/protocolo/documentos'},
  {'put': '/protocolo/documentos'},
  {'delete': '/protocolo/documentos'},
  {'get': '/protocolo/atributosprotocolo'},
  {'post': '/protocolo/atributosprotocolo'},
  {'put': '/protocolo/atributosprotocolo'},
  {'delete': '/protocolo/atributosprotocolo'},
  {'get': '/protocolo/processos/public/site/:anoExercicio/:codProcesso'},
];

Middleware logRequestsCustom(
        {void Function(String message, bool isError)? logger}) =>
    (innerHandler) {
      final theLogger = logger ?? _defaultLogger;
      return (request) {
        var startTime = DateTime.now();
        var watch = Stopwatch()..start();
        return Future.sync(() => innerHandler(request)).then((response) {
          var msg = _message(startTime, response.statusCode,
              request.requestedUri, request.method, watch.elapsed);
          theLogger(msg, false);
          return response;
        }, onError: (Object error, StackTrace stackTrace) {
          if (error is HijackException) throw error;
          var msg = _errorMessage(startTime, request.requestedUri,
              request.method, watch.elapsed, error, stackTrace);
          theLogger(msg, true);
          // ignore: only_throw_errors
          throw error;
        });
      };
    };

String _formatQuery(String query) {
  return query == '' ? '' : '?$query';
}

String _message(DateTime requestTime, int statusCode, Uri requestedUri,
    String method, Duration elapsedTime) {
  return '${requestTime.toIso8601String()} '
      '${elapsedTime.toString().padLeft(15)} '
      '${method.padRight(7)} [$statusCode] ' // 7 - longest standard HTTP method
      '${requestedUri.path}${_formatQuery(requestedUri.query)}'
      '  isolate: ${Isolate.current.debugName}';
}

String _errorMessage(DateTime requestTime, Uri requestedUri, String method,
    Duration elapsedTime, Object error, StackTrace? stack) {
  var chain = Chain.current();
  if (stack != null) {
    chain = Chain.forTrace(stack)
        .foldFrames((frame) => frame.isCore || frame.package == 'shelf')
        .terse;
  }

  var msg = '$requestTime\t$elapsedTime\t$method\t${requestedUri.path}'
      '${_formatQuery(requestedUri.query)}\n$error';

  return '$msg\n$chain';
}

void _defaultLogger(String msg, bool isError) {
  if (isError) {
    print('[ERROR] $msg');
  } else {
    print(msg);
  }
}
codekeyz commented 10 months ago

Hey @insinfo, I love shelf for what they were able to build. I'm building this Backend Framework that should let you do this seamlessly. Also, it allows you to use Shelf Middle-wares directly.

Peep the repository and let me know what you think. https://github.com/codekeyz/pharaoh

Also, I am looking for contributors. 👋

insinfo commented 10 months ago

@codekeyz Although I admire your dedication to creating another framework, I think that instead of creating another framework we could join efforts in developing the angel framework here https://github.com/dukefirehawk/angel